If you want to build chatbot next.js vercel ai sdk tutorial that actually streams tokens and handles multi-turn context, you need more than a fetch wrapper. This guide walks through a production-shaped implementation using the App Router, the AI SDK’s server streaming, and the useChat hook on the client. We’ll stand up a working full-stack chatbot in about 20 minutes and verify each layer as we go.
Prerequisites
- Node.js 18.17+ (Node 20 recommended)
- A package manager (
npmorpnpm) - An OpenAI API key, or any OpenAI-compatible endpoint credentials
- Basic familiarity with React Server Components, TypeScript, and Next.js routing
Set your key in the shell before starting:
export OPENAI_API_KEY=sk-...
If you plan to route through a gateway later, you can override the base URL after the app is built.
Scaffold the Next.js app
Use the official scaffolder with TypeScript, App Router, Tailwind, and a src directory:
npx create-next-app@latest chatbot --ts --app --eslint --tailwind --src-dir --import-alias "@/*"
cd chatbot
The flags avoid interactive prompts. After install, add the AI SDK core and the OpenAI provider package:
npm install ai @ai-sdk/openai zod
zod is not strictly required for a minimal bot, but we’ll use it to validate inbound requests later.
Checkpoint: Run npm run dev and open http://localhost:3000. You should see the default Next.js starter. Stop the dev server; we’ll replace the page.
Server: streaming route
Create src/app/api/chat/route.ts. The AI SDK’s streamText handles Server-Sent Events framing, backpressure, and cancellation. We return toDataStreamResponse() because that matches the protocol useChat consumes.
import { openai } from '@ai-sdk/openai';
import { streamText } from 'ai';
export const runtime = 'edge';
export const maxDuration = 30;
export async function POST(req: Request) {
const { messages } = await req.json();
const result = streamText({
model: openai('gpt-4o-mini'),
messages,
system: 'You are a concise coding assistant.',
});
return result.toDataStreamResponse();
}
The system prompt is optional but useful to constrain behavior. The edge runtime reduces cold-start latency; switch to nodejs if you need Node-specific APIs.
Checkpoint: Test the route with curl before writing any UI.
curl -X POST http://localhost:3000/api/chat \
-H "Content-Type: application/json" \
-d '{"messages":[{"role":"user","content":"Say hello in one word"}]}'
Expected raw output is a stream of data frames:
0:"Hi"
0:" there"
Each 0:"..." chunk is a text delta. If you see a JSON error, check that OPENAI_API_KEY is exported in the shell running next dev.
Client: useChat wired to the route
Replace src/app/page.tsx entirely. Mark it a client component and use the useChat hook:
'use client';
import { useChat } from 'ai/react';
export default function Chat() {
const { messages, input, handleInputChange, handleSubmit, isLoading, error } =
useChat();
return (
<div className="mx-auto max-w-xl p-4">
<div className="space-y-2 mb-4">
{messages.map((m) => (
<div
key={m.id}
className={m.role === 'user' ? 'text-right' : 'text-left'}
>
<span className="inline-block rounded bg-neutral-100 px-2 py-1">
{m.content}
</span>
</div>
))}
{isLoading && (
<div className="text-xs text-neutral-400">Streaming…</div>
)}
{error && (
<div className="text-xs text-red-500">Failed: {error.message}</div>
)}
</div>
<form onSubmit={handleSubmit} className="flex gap-2">
<input
className="flex-1 border rounded px-2 py-1"
value={input}
onChange={handleInputChange}
placeholder="Type a message..."
/>
<button type="submit" className="border rounded px-3 py-1">
Send
</button>
</form>
</div>
);
}
useChat manages the message array, serializes the POST body, and parses the streamed response. It does not require you to write a single fetch call.
Checkpoint: Start npm run dev, open the page, and send “Hello”. The assistant replies inline. In the browser Network tab, you’ll see a POST /api/chat with Content-Type: text/plain; charset=utf-8 and a streaming body.
This is the core of the build chatbot next.js vercel ai sdk tutorial: a streaming server and a hook-driven client.
Validating input
Never trust the client. Add zod validation in the route:
import { z } from 'zod';
import { openai } from '@ai-sdk/openai';
import { streamText } from 'ai';
const schema = z.object({
messages: z.array(
z.object({
role: z.enum(['user', 'assistant', 'system']),
content: z.string(),
})
),
});
export async function POST(req: Request) {
const body = await req.json();
const { messages } = schema.parse(body);
const result = streamText({
model: openai('gpt-4o-mini'),
messages,
});
return result.toDataStreamResponse();
}
A malformed request now returns a 400 before hitting the model.
Persisting conversations
useChat keeps state in memory. To persist, use the onFinish callback:
'use client';
import { useChat } from 'ai/react';
export default function Chat() {
const { messages, input, handleInputChange, handleSubmit } = useChat({
onFinish: async (message) => {
await fetch('/api/conversations', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ messages }),
});
},
});
// ... same JSX
}
Your /api/conversations route can write to Postgres, DynamoDB, or a file. The key point: the client already has the full messages array, so you avoid re-serializing the stream.
Swapping providers without code changes
The @ai-sdk/openai provider reads OPENAI_BASE_URL and OPENAI_API_KEY from the environment. To front 240+ models with automatic fallback when a provider is rate-limited or degraded, point the base URL at a gateway:
export OPENAI_BASE_URL=https://api.n4n.ai/v1
export OPENAI_API_KEY=your-gateway-key
The same openai('gpt-4o-mini') call now resolves through n4n.ai, which honors client routing directives and forwards provider cache-control hints. You get per-token usage metering without adding instrumentation code. This keeps the build chatbot next.js vercel ai sdk tutorial code portable across inference vendors.
Hardening for production
- Set
export const maxDuration = 30;on the route (already shown) to match Vercel’s default function limit. - Use
runtime = 'nodejs'if you need secrets managers or filesystem access. - Add CORS headers only if a non-Next.js domain will call the route.
- Consider rate limiting via Upstash or Clerk on the
/api/chatpath.
A minimal hardened route header block:
export const runtime = 'edge';
export const maxDuration = 30;
export const dynamic = 'force-dynamic';
Final verification
Run a production build to catch type errors:
npm run build
Expected output ends with:
✓ Compiled successfully
✓ Collecting page data
✓ Generating static pages (4/4)
Deploy with vercel deploy (or your platform of choice). The chatbot streams, surfaces errors, and can be repointed to any OpenAI-compatible backend by changing two environment variables.
You now have a runnable, typed, streaming full-stack chatbot built on Next.js and the Vercel AI SDK.