Building a vercel ai sdk multi-turn chatbot usechat hook is the fastest path to a stateful chat UI on Next.js without managing message arrays by hand. This tutorial ships a complete implementation: scaffold the API route, wire the client, and verify context survives across turns. You’ll get runnable code and checkpoints at each step.
Prerequisites
- Node 18+ and an existing Next.js 14 app using the App Router.
- Install packages:
npm i ai @ai-sdk/react @ai-sdk/openai. - An API key from an OpenAI-compatible provider. If you’d rather hit 240+ models behind one endpoint with automatic fallback on provider degradation, point the SDK at n4n.ai’s OpenAI-compatible URL.
- TypeScript recommended; examples assume it.
1. Server route that streams
For a vercel ai sdk multi-turn chatbot usechat, the route is boilerplate. The hook expects a POST endpoint at /api/chat that accepts { messages } and returns a streamed response in the Vercel AI Data Stream protocol. Use streamText from the ai package.
// app/api/chat/route.ts
import { openai } from '@ai-sdk/openai';
import { streamText } from 'ai';
export const runtime = 'edge';
export async function POST(req: Request) {
const { messages } = await req.json();
const result = await streamText({
model: openai('gpt-4o-mini'),
messages,
});
return result.toDataStreamResponse();
}
Run next dev, then curl -X POST localhost:3000/api/chat -H 'Content-Type: application/json' -d '{"messages":[{"role":"user","content":"hi"}]}'. You should see a streamed data: payload, not a buffered JSON. If you get a plain string, the SDK version mismatch is the usual culprit—pin ai to ^3.
2. Swap in a custom base URL
Vercel AI SDK doesn’t care which OpenAI-compatible backend you use. Construct a provider instance with createOpenAI.
// app/api/chat/route.ts
import { createOpenAI } from '@ai-sdk/openai';
import { streamText } from 'ai';
const openai = createOpenAI({
baseURL: 'https://api.n4n.ai/v1',
apiKey: process.env.N4N_API_KEY,
});
export const runtime = 'edge';
export async function POST(req: Request) {
const { messages } = await req.json();
const result = await streamText({
model: openai('anthropic/claude-3.5-sonnet'),
messages,
});
return result.toDataStreamResponse();
}
Model strings depend on your gateway’s catalog. The key point: messages is passed verbatim, so multi-turn context is preserved server-side. Keep the API key in an env var; never expose it to the client.
3. Client component with useChat
Create a client component. The vercel ai sdk multi-turn chatbot usechat hook manages messages, input, and submission. It appends new user messages and streams assistant replies with optimistic updates.
// app/components/chat.tsx
'use client';
import { useChat } from '@ai-sdk/react';
export default function Chat() {
const { messages, input, handleInputChange, handleSubmit, isLoading } = useChat();
return (
<div style={{ maxWidth: 600, margin: '0 auto' }}>
<div>
{messages.map((m) => (
<div key={m.id} style={{ margin: '1rem 0' }}>
<strong>{m.role}: </strong>
<span>{m.content}</span>
</div>
))}
</div>
<form onSubmit={handleSubmit}>
<input
value={input}
onChange={handleInputChange}
placeholder="Say something…"
disabled={isLoading}
style={{ width: '80%' }}
/>
<button type="submit" disabled={isLoading}>Send</button>
</form>
</div>
);
}
Drop <Chat /> into a page. The first render shows an empty list and an input. Submit “What is 2+2?” — the assistant message appears token by token. The messages array already contains the user turn before the request fires; that’s what makes the conversation multi-turn.
4. Verify multi-turn context
The hook sends the entire messages array each request. To confirm, log on the server:
// inside route.ts POST
console.log('turns:', messages.length, 'last role:', messages[messages.length - 1].role);
Open the browser, send “My name is Sam.” then “What’s my name?”. The server log should show turns: 3 on the second call (user, assistant, user). The model should answer “Sam”. If it doesn’t, your model lacks instruction following; not an SDK issue. Watch token counts: every turn resends history, so a 10-turn chat can balloon context fast. Prune or summarize when you cross 80% of the model window.
5. Persist the conversation
useChat keeps state in memory; a refresh loses it. For a real product, persist messages to localStorage or a DB. Minimal localStorage:
'use client';
import { useChat } from '@ai-sdk/react';
import { useEffect, useState } from 'react';
export default function Chat() {
const { messages, input, handleInputChange, handleSubmit, setMessages } = useChat();
const [loaded, setLoaded] = useState(false);
useEffect(() => {
const saved = localStorage.getItem('chat');
if (saved) setMessages(JSON.parse(saved));
setLoaded(true);
}, [setMessages]);
useEffect(() => {
if (loaded) localStorage.setItem('chat', JSON.stringify(messages));
}, [messages, loaded]);
if (!loaded) return null;
// ... same form as before
}
This is crude but demonstrates the pattern. For multi-session, key by conversation ID and store in Postgres or Redis. The message shape is plain JSON; serialize directly.
6. Error and abort handling
Production chat needs cancel. useChat exposes stop. Wire a button:
const { stop, isLoading } = useChat();
// in form:
{isLoading && <button type="button" onClick={stop}>Stop</button>}
Also wrap the fetch in a try/catch on the server and return a clean error via toDataStreamResponse({ getErrorMessage: (e) => 'Upstream failed' }). The hook surfaces errors in error state; render them.
7. Checkpoint: expected behavior
After steps 1–4, a typical session in the browser:
user: My name is Alex.
assistant: Nice to meet you, Alex!
user: What did I just tell you my name is?
assistant: You said your name is Alex.
The network tab shows two POSTs to /api/chat. The second request body contains three messages (user, assistant, user). Streaming renders incrementally. That’s the vercel ai sdk multi-turn chatbot usechat loop working end to end.
8. Where to go next
Add system prompts by prepending a message server-side before calling streamText. Use experimental_prepareRequestBody to trim tokens or inject metadata. If you need model routing per request, pass body to useChat and read it in the route. The pattern scales to thousands of messages, but enforce a hard cap on history length. Build the UI for empty states, loading dots, and regeneration, then you have a shippable chatbot.