Shipping a next.js deploy vercel ai chat streaming app requires more than a static build—you need serverless functions that stream tokens without buffering. This guide walks through a production-grade setup using the App Router and Vercel AI SDK, from local scaffold to a live URL with verified token streaming.
Step 1: Scaffold the Next.js App Router project
Start with a clean App Router project. The Pages Router can stream too, but App Router route handlers give cleaner access to the Web Streams API and avoid deprecated patterns.
npx create-next-app@latest chat-app --ts --app --tailwind --eslint --src-dir --import-alias "@/*"
cd chat-app
Use TypeScript. The --src-dir flag keeps app/ under src/, which matches most production layouts. If you skip it, adjust import paths accordingly.
Run npm run dev and confirm the default page loads on localhost:3000. That validates your Node version and Next build chain before adding LLM complexity. Do not skip this—debugging streaming issues on top of a broken toolchain wastes hours.
Step 2: Install the Vercel AI SDK and provider
The Vercel AI SDK abstracts streaming, message formatting, and provider differences. For an OpenAI-compatible backend, install the core package and the OpenAI adapter.
npm install ai @ai-sdk/openai zod
zod is optional but the SDK uses it for structured outputs; including it avoids later type friction. If your model provider exposes an OpenAI-compatible chat completions endpoint, you do not need a custom adapter.
Provider compatibility note
Most gateways speak the OpenAI chat protocol. You can swap the base URL at runtime without changing call sites. For example, a single OpenAI-compatible endpoint that addresses 240+ models with automatic fallback when a provider is rate-limited can replace the default OpenAI host by setting baseURL on the provider. This keeps your route code stable while you experiment with different models behind one contract.
Step 3: Write the streaming route handler
Create src/app/api/chat/route.ts. This file receives messages from the client and returns a streamed response.
import { openai } from '@ai-sdk/openai';
import { streamText } from 'ai';
export const runtime = 'nodejs';
export const maxDuration = 30;
export async function POST(req: Request) {
const { messages } = await req.json();
const result = streamText({
model: openai('gpt-4o-mini'),
messages,
});
return result.toDataStreamResponse();
}
Key details:
runtime = 'nodejs'keeps you on the Node.js runtime where most provider SDKs are tested. Useedgeonly if you need low cold starts and your provider supports edge fetch.maxDurationtells Vercel the function may run up to 30 seconds. Without it, the platform defaults to 10s on some plans and will kill mid-stream.toDataStreamResponse()emits the SDK’s wire format thatuseChatexpects. Do not hand-roll aReadableStreamunless you need custom protocols.
The SDK also supports toTextStreamResponse() if you want raw text chunks, but then you lose the built-in error and tool-call framing. Stick with the data stream for chat UIs.
Step 4: Build the client chat interface
The SDK ships useChat, a hook that manages message state and reads the stream. Replace src/app/page.tsx with a minimal chat UI.
'use client';
import { useChat } from 'ai/react';
export default function Chat() {
const { messages, input, handleInputChange, handleSubmit, error, reload } = useChat();
return (
<div className="mx-auto max-w-lg p-4">
<div className="space-y-2">
{messages.map((m) => (
<div key={m.id} className="whitespace-pre-wrap">
<span className="font-bold">{m.role}: </span>
{m.content}
</div>
))}
</div>
{error && (
<div className="text-red-500">
Failed. <button onClick={reload}>Retry</button>
</div>
)}
<form onSubmit={handleSubmit} className="mt-4 flex gap-2">
<input
value={input}
onChange={handleInputChange}
className="flex-1 rounded border p-2"
placeholder="Say something..."
/>
<button type="submit" className="rounded bg-black px-4 py-2 text-white">
Send
</button>
</form>
</div>
);
}
The hook posts to /api/chat by default. It appends tokens as they arrive, so the UI updates incrementally without a loading spinner hack. It also exposes stop to abort an in-flight stream, which you should bind to a cancel button in real products.
Styling and errors
Tailwind is already configured from Step 1. If a stream errors, useChat exposes error and reload. Wire those into the UI before shipping, as shown above. Silent failures are the most common complaint in production chat apps.
Step 5: Configure environment and model base URL
Never hardcode keys. Create .env.local:
OPENAI_API_KEY=sk-your-real-key
If you use a gateway, set the base URL in the provider constructor inside the route:
import { createOpenAI } from '@ai-sdk/openai';
const provider = createOpenAI({
baseURL: 'https://api.n4n.ai/v1',
apiKey: process.env.OPENAI_API_KEY,
});
const result = streamText({ model: provider('gpt-4o-mini'), messages });
This keeps the same streamText call shape. The gateway forwards provider cache-control hints and meters per-token usage, so your route code stays unchanged when you switch models. You can also pass a model string from the request body to let the client pick among 240+ models without redeploying.
Make sure .env.local is git-ignored. Next.js does this by default, but verify with grep OPENAI .gitignore.
Step 6: Deploy to Vercel
Push to GitHub, then link the repo in the Vercel dashboard or use the CLI:
npm install -g vercel
vercel deploy --prod
Vercel detects Next.js automatically. After the build, set environment variables in the project settings. They must match the names in .env.local exactly.
Function region and timeout
Streaming latency improves if the function runs in the same region as your users. Set regions in vercel.json:
{
"functions": {
"src/app/api/chat/route.ts": {
"maxDuration": 30,
"regions": ["iad1"]
}
}
}
Without this, Vercel may deploy to a single default region and add cross-continent hops to your LLM provider. Also confirm your plan allows the configured maxDuration; Hobby enforces lower caps.
Step 7: Verify streaming works
Local verification is not enough. After deploy, hit the live API with curl and watch for incremental chunks:
curl -N -X POST https://your-app.vercel.app/api/chat \
-H "Content-Type: application/json" \
-d '{"messages":[{"role":"user","content":"Write a haiku about latency"}]}'
The -N flag disables curl buffering. You should see multiple data: frames arrive over time, not one blob at the end. If you get a single response after several seconds, the stream is being buffered by a proxy or middleware.
In the browser, open the deployed URL and send a message. Open DevTools → Network → /api/chat → Response. The stream should show tokens appended live. React DevTools will show messages array growing in length on each token.
Common failure modes
- Vercel Hobby plan timeout: Functions cut at 10s unless
maxDurationis set and plan allows more. - Missing
runtimeexport: Some Next versions infer edge and break Node-only provider SDKs. - CORS: If you call the route from a different origin, add
corsheaders or keep the same origin. - Wrong response type: Returning
result.text()instead oftoDataStreamResponse()silently disables streaming.
Step 8: Harden for production
Add input validation with zod to the route:
import { z } from 'zod';
const schema = z.object({
messages: z.array(z.object({ role: z.string(), content: z.string() })),
});
export async function POST(req: Request) {
const body = await req.json();
const { messages } = schema.parse(body);
// ...
}
This rejects malformed requests before they hit the model. Also consider rate limiting per IP using Vercel’s @vercel/edge-config or a KV store. Streaming endpoints are abuse-prone because they hold connections open.
For observability, log the finish reason and token count from streamText with an onFinish callback:
const result = streamText({
model: openai('gpt-4o-mini'),
messages,
onFinish: ({ usage }) => {
console.log('tokens', usage);
},
});
Those logs appear in Vercel Function logs and help track cost. If you use a gateway, per-token metering is already handled upstream, but logging locally still validates your client behavior.
Step 9: Extend the pattern
The same structure supports tool calls, multi-modal input, and structured generation. Swap streamText for streamObject when you need typed JSON streams. The client hook useChat can be replaced with useCompletion for single-prompt flows.
You now have a next.js deploy vercel ai chat streaming app that renders tokens live, runs on serverless functions, and degrades cleanly on errors. The remaining work is product-specific: persistence, auth, and evaluation.