n4nAI

Building a multi-turn chatbot with Vercel AI SDK's useChat

Hands-on tutorial to build a vercel ai sdk multi-turn chatbot usechat in Next.js: scaffold route, wire client, stream, persist, and verify context.

n4n Team3 min read604 words

Audio narration

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

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.

Tagsvercel-ai-sdkusechatmulti-turnchatbot

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 →