n4nAI

Build a full-stack chatbot with Next.js and Vercel AI SDK

Full-stack step-by-step guide to build chatbot next.js vercel ai sdk tutorial: scaffold app, stream responses, and wire a typed UI with useChat for production.

n4n Team3 min read615 words

Audio narration

Coming soon — every post will get a voice note here.

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 (npm or pnpm)
  • 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/chat path.

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.

Tagsvercel-ai-sdknext-jschatbottutorial

Written by

n4n Team

The team building n4n — a single OpenAI-compatible API in front of 240+ models, with automatic fallback, load balancing and pay-per-token metering.

More from n4n Team →

All building chatbots with vercel ai sdk & next.js posts →