AI SDK (with Next.js)
AI SDK streams model responses to the browser, and a Next.js route handler gives you a place to persist each exchange with Prisma ORM, so a conversation survives page reloads.
In this guide, you'll learn to build a chat application using AI SDK with Next.js and Prisma ORM to store chat sessions and messages in a Prisma Postgres database. You can find a complete example of this guide on GitHub.
- Node.js 20+
- An OpenAI API key or other AI provider API key
To get started, you'll need to create a new Next.js project.
bunx create-next-app@latest ai-sdk-prismapnpm dlx create-next-app@latest ai-sdk-prismayarn dlx create-next-app@latest ai-sdk-prismanpx create-next-app@latest ai-sdk-prismaIt will prompt you to customize your setup. Choose the defaults:
Navigate to the project directory:
cd ai-sdk-prismaTo get started with Prisma, you'll need to install a few dependencies:
bun add prisma@prev tsx @types/pg --devpnpm add prisma@prev tsx @types/pg --save-devyarn add prisma@prev tsx @types/pg --devnpm install prisma@prev tsx @types/pg --save-devbun add @prisma/client@7 @prisma/adapter-pg dotenv pgpnpm add @prisma/client@7 @prisma/adapter-pg dotenv pgyarn add @prisma/client@7 @prisma/adapter-pg dotenv pgnpm install @prisma/client@7 @prisma/adapter-pg dotenv pg[!NOTE] If you are using a different database provider (MySQL, SQL Server, SQLite), install the corresponding driver adapter package instead of
@prisma/adapter-pg. For more information, see Database drivers.
Once installed, initialize Prisma in your project:
Once installed, initialize Prisma in your project:
bunx --bun prisma init --output ../app/generated/prismapnpm prisma init --output ../app/generated/prismayarn prisma init --output ../app/generated/prismanpx prisma init --output ../app/generated/prisma[!NOTE]
prisma initcreates the Prisma scaffolding and a localDATABASE_URL. In the next step, you will create a Prisma Postgres database and replace that value with a directpostgres://...connection string.
This will create:
- A
prismadirectory with aschema.prismafile. - A
prisma.config.tsfile for configuring Prisma - A
.envfile containing a localDATABASE_URLat the project root. - The
outputfield specifies where the generated Prisma Client will be stored.
Create a Prisma Postgres database and replace the generated DATABASE_URL in your .env file with the postgres://... connection string from the CLI output:
This will create:
- A
prismadirectory with aschema.prismafile. - A
prisma.config.tsfile for configuring Prisma - A
.envfile containing a localDATABASE_URLat the project root. - The
outputfield specifies where the generated Prisma Client will be stored.
Create a Prisma Postgres database and replace the generated DATABASE_URL in your .env file with the postgres://... connection string from the CLI output:
bunx create-dbpnpm dlx create-dbyarn dlx create-dbnpx create-dbIn the prisma/schema.prisma file, add the following models:
generator client {
provider = "prisma-client"
output = "../app/generated/prisma"
}
datasource db {
provider = "postgresql"
}
model Session {
id String @id
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
messages Message[]
}
model Message {
id String @id @default(cuid())
role MessageRole
content String
createdAt DateTime @default(now())
sessionId String
session Session @relation(fields: [sessionId], references: [id], onDelete: Cascade)
}
enum MessageRole {
USER
ASSISTANT
} This creates three models: Session, Message, and MessageRole.
To get access to the variables in the .env file, they can either be loaded by your runtime, or by using dotenv. Include an import for dotenv at the top of the prisma.config.ts
import "dotenv/config";
import { defineConfig, env } from "prisma/config";
export default defineConfig({
schema: "prisma/schema.prisma",
migrations: {
path: "prisma/migrations",
},
datasource: {
url: env("DATABASE_URL"),
},
});2.4. Configure the Prisma Client generator
Section titled “2.4. Configure the Prisma Client generator”Now, run the following command to create the database tables and generate the Prisma Client:
bunx prisma migrate dev --name initpnpm prisma migrate dev --name inityarn prisma migrate dev --name initnpx prisma migrate dev --name initbunx prisma generatepnpm prisma generateyarn prisma generatenpx prisma generateCreate a /lib directory and a prisma.ts file inside it. This file will be used to create and export your Prisma Client instance.
mkdir lib
touch lib/prisma.tsSet up the Prisma client like this:
import { PrismaClient } from "../app/generated/prisma/client";
import { PrismaPg } from "@prisma/adapter-pg";
const adapter = new PrismaPg({
connectionString: process.env.DATABASE_URL!,
});
const globalForPrisma = global as unknown as {
prisma: PrismaClient;
};
const prisma =
globalForPrisma.prisma ||
new PrismaClient({
adapter,
});
if (process.env.NODE_ENV !== "production") globalForPrisma.prisma = prisma;
export default prisma;Install the AI SDK package:
bun add ai @ai-sdk/react @ai-sdk/openai zodpnpm add ai @ai-sdk/react @ai-sdk/openai zodyarn add ai @ai-sdk/react @ai-sdk/openai zodnpm install ai @ai-sdk/react @ai-sdk/openai zodTo use AI SDK, you'll need to obtain an API key from OpenAI.
- Navigate to OpenAI API Keys
- Click on
Create new secret key - Fill in the form:
- Give your key a name like
Next.js AI SDK Project - Select
Allaccess
- Give your key a name like
- Click on
Create secret key - Copy the API key
- Add the API key to the
.envfile:
DATABASE_URL=<YOUR_DATABASE_URL_HERE>
OPENAI_API_KEY=<YOUR_OPENAI_API_KEY_HERE>To use AI SDK, you'll need to obtain an API key from OpenAI.
-
Navigate to OpenAI API Keys
-
Click on
Create new secret key -
Fill in the form:
- Give your key a name like
Next.js AI SDK Project - Select
Allaccess
- Give your key a name like
-
Click on
Create secret key -
Copy the API key
-
Add the API key to the
.envfile:
DATABASE_URL=<YOUR_DATABASE_URL_HERE>
OPENAI_API_KEY=<YOUR_OPENAI_API_KEY_HERE>You need to create a route handler to handle the AI SDK requests. This handler will process chat messages and stream AI responses back to the client.
mkdir -p app/api/chat
touch app/api/chat/route.tsSet up the basic route handler:
import { openai } from "@ai-sdk/openai";
import { streamText, UIMessage, convertToModelMessages } from "ai";
export const maxDuration = 300;
export async function POST(req: Request) {
const { messages }: { messages: UIMessage[] } = await req.json();
const result = streamText({
model: openai("gpt-4o"),
messages: convertToModelMessages(messages),
});
return result.toUIMessageStreamResponse();
}This route handler:
- Extracts the conversation history from the request body
- Converts UI messages to the format expected by the AI model
- Streams the AI response back to the client in real-time
To save chat sessions and messages to the database, we need to:
- Add a session
idparameter to the request - Include an
onFinishcallback in the response - Pass the
idandmessagesparameters to thesaveChatfunction (which we'll build next)
import { openai } from "@ai-sdk/openai";
import { streamText, UIMessage, convertToModelMessages } from "ai";
import { saveChat } from "@/lib/save-chat";
export const maxDuration = 300;
export async function POST(req: Request) {
const { messages, id }: { messages: UIMessage[]; id: string } = await req.json();
const result = streamText({
model: openai("gpt-4o"),
messages: convertToModelMessages(messages),
});
return result.toUIMessageStreamResponse({
originalMessages: messages,
onFinish: async ({ messages }) => {
await saveChat(messages, id);
},
});
}Create a new file at lib/save-chat.ts to save the chat sessions and messages to the database:
touch lib/save-chat.tsTo start, create a basic function called saveChat that will be used to save the chat sessions and messages to the database.
Pass into it the messages and id parameters typed as UIMessage[] and string respectively:
import { UIMessage } from "ai";
export async function saveChat(messages: UIMessage[], id: string) {}Now, add the logic to create a session with the given id:
import prisma from "./prisma";
import { UIMessage } from "ai";
export async function saveChat(messages: UIMessage[], id: string) {
const session = await prisma.session.upsert({
where: { id },
update: {},
create: { id },
});
if (!session) throw new Error("Session not found");
}Add the logic to save the messages to the database. You'll only be saving the last two messages (Users and Assistants last messages) to avoid any overlapping messages.
import prisma from "./prisma";
import { UIMessage } from "ai";
export async function saveChat(messages: UIMessage[], id: string) {
const session = await prisma.session.upsert({
where: { id },
update: {},
create: { id },
});
if (!session) throw new Error("Session not found");
const lastTwoMessages = messages.slice(-2);
for (const msg of lastTwoMessages) {
let content = JSON.stringify(msg.parts);
if (msg.role === "assistant") {
const textParts = msg.parts.filter((part) => part.type === "text");
content = JSON.stringify(textParts);
}
await prisma.message.create({
data: {
role: msg.role === "user" ? "USER" : "ASSISTANT",
content: content,
sessionId: session.id,
},
});
}
}This function:
- Upserts a session with the given
idto create a session if it doesn't exist - Saves the messages to the database under the
sessionId
Create a new file at app/api/messages/route.ts to fetch the messages from the database:
mkdir -p app/api/messages
touch app/api/messages/route.tsCreate a basic API route to fetch the messages from the database.
import { NextResponse } from "next/server";
import prisma from "@/lib/prisma";
export async function GET() {
try {
const messages = await prisma.message.findMany({
orderBy: { createdAt: "asc" },
});
const uiMessages = messages.map((msg) => ({
id: msg.id,
role: msg.role.toLowerCase(),
parts: JSON.parse(msg.content),
}));
return NextResponse.json({ messages: uiMessages });
} catch (error) {
console.error("Error fetching messages:", error);
return NextResponse.json({ messages: [] });
}
}Replace the content of the app/page.tsx file with the following:
"use client";
export default function Page() {}Start by importing the required dependencies and setting up the state variables that will manage the chat interface:
"use client";
import { useChat } from "@ai-sdk/react";
import { useState, useEffect } from "react";
export default function Chat() {
const [input, setInput] = useState("");
const [isLoading, setIsLoading] = useState(true);
const { messages, sendMessage, setMessages } = useChat();
}Create a useEffect hook that will automatically fetch and display any previously saved messages when the chat component loads:
"use client";
import { useChat } from "@ai-sdk/react";
import { useState, useEffect } from "react";
export default function Chat() {
const [input, setInput] = useState("");
const [isLoading, setIsLoading] = useState(true);
const { messages, sendMessage, setMessages } = useChat();
useEffect(() => {
fetch("/api/messages")
.then((res) => res.json())
.then((data) => {
if (data.messages && data.messages.length > 0) {
setMessages(data.messages);
}
setIsLoading(false);
})
.catch(() => setIsLoading(false));
}, [setMessages]);
}This loads any existing messages from your database when the component first mounts, so users can see their previous conversation history.
Build the UI components that will show a loading indicator while fetching data and render the chat messages with proper styling:
'use client';import { useChat } from '@ai-sdk/react';import { useState, useEffect } from 'react';export default function Chat() { const [input, setInput] = useState(''); const [isLoading, setIsLoading] = useState(true); const { messages, sendMessage, setMessages } = useChat(); useEffect(() => { fetch('/api/messages') .then(res => res.json()) .then(data => { if (data.messages && data.messages.length > 0) { setMessages(data.messages); } setIsLoading(false); }) .catch(() => setIsLoading(false)); }, [setMessages]); if (isLoading) { return <div className="flex justify-center items-center h-screen">Loading...</div>; } return ( <div className="flex flex-col w-full max-w-md py-24 mx-auto stretch"> {messages.map(message => ( <div key={message.id} className={`flex ${message.role === 'user' ? 'justify-end' : 'justify-start'} mb-4`}> <div className={`max-w-[80%] rounded-lg px-4 py-3 ${ message.role === 'user' ? 'bg-neutral-600 text-white' : 'bg-neutral-200 dark:bg-neutral-800 text-neutral-900 dark:text-neutral-100' }`}> <div className="whitespace-pre-wrap"> <p className="text-xs font-extralight mb-1 opacity-70">{message.role === 'user' ? 'YOU ' : 'AI '}</p> {message.parts.map((part, i) => { switch (part.type) { case 'text': return <div key={`${message.id}-${i}`}>{part.text}</div>; } })} </div> </div> </div> ))}The message rendering logic handles different message types and applies appropriate styling - user messages appear on the right with a dark background, while AI responses appear on the left with a light background.
Now we need to create the input interface that allows users to type and send messages to the AI:
"use client";import { useChat } from "@ai-sdk/react";import { useState, useEffect } from "react";export default function Chat() { const [input, setInput] = useState(""); const [isLoading, setIsLoading] = useState(true); const { messages, sendMessage, setMessages } = useChat(); useEffect(() => { fetch("/api/messages") .then((res) => res.json()) .then((data) => { if (data.messages && data.messages.length > 0) { setMessages(data.messages); } setIsLoading(false); }) .catch(() => setIsLoading(false)); }, [setMessages]); if (isLoading) { return <div className="flex justify-center items-center h-screen">Loading...</div>; } return ( <div className="flex flex-col w-full max-w-md py-24 mx-auto stretch"> {messages.map((message) => ( <div key={message.id} className={`flex ${message.role === "user" ? "justify-end" : "justify-start"} mb-4`} > <div className={`max-w-[80%] rounded-lg px-4 py-3 ${ message.role === "user" ? "bg-neutral-600 text-white" : "bg-neutral-200 dark:bg-neutral-800 text-neutral-900 dark:text-neutral-100" }`} > <div className="whitespace-pre-wrap"> <p className="text-xs font-extralight mb-1 opacity-70"> {message.role === "user" ? "YOU " : "AI "} </p> {message.parts.map((part, i) => { switch (part.type) { case "text": return <div key={`${message.id}-${i}`}>{part.text}</div>; } })} </div> </div> </div> ))} <form onSubmit={(e) => { e.preventDefault(); sendMessage({ text: input }); setInput(""); }} > <input className="fixed dark:bg-zinc-900 bottom-0 w-full max-w-md p-2 mb-8 border border-zinc-300 dark:border-zinc-800 rounded shadow-xl" value={input} placeholder="Say something..." onChange={(e) => setInput(e.currentTarget.value)} /> </form> </div> );}To test your application, run the following command:
bun run devpnpm run devyarn devnpm run devOpen your browser and navigate to http://localhost:3000 to see your application in action.
Test it by sending a message to the AI and see if it's saved to the database. Check Prisma Studio to see the messages in the database.
Open your browser and navigate to http://localhost:3000 to see your application in action.
Test it by sending a message to the AI and see if it's saved to the database. Check Prisma Studio to see the messages in the database.
bunx prisma studiopnpm prisma studioyarn prisma studionpx prisma studioYour AI SDK chat application now stores every session and message in Prisma Postgres and reloads them when the page opens.
Your AI SDK chat application now stores every session and message in Prisma Postgres and reloads them when the page opens.
Now that you have a working AI SDK chat application connected to a Prisma Postgres database, you can:
- Extend your Prisma schema with more models and relationships
- Add create/update/delete routes and forms
- Explore authentication and validation