This usechat next.js chat app n4n.ai tutorial builds a streaming chat UI with the Vercel AI SDK’s useChat hook backed by a single OpenAI-compatible endpoint. You’ll stand up a Next.js App Router project, proxy messages to a gateway that routes across many models, and render tokens as they arrive.
Prerequisites
- Node.js 18.17+ and npm
- A Next.js 14+ project using the App Router
- An API key for the gateway (set as
N4N_API_KEY) - Basic familiarity with React client components
Scaffold a fresh project if you don’t have one:
npx create-next-app@latest chat-app --ts --app --no-tailwind --eslint
cd chat-app
Install the Vercel AI SDK
Add the packages that handle streaming and the OpenAI-compatible provider:
npm install ai @ai-sdk/react @ai-sdk/openai
ai provides streamText for server routes. @ai-sdk/react ships useChat. @ai-sdk/openai lets you repoint the OpenAI client at any compliant base URL.
Backend: streaming route
Create app/api/chat/route.ts. This server module keeps your key out of the browser.
import { streamText } from 'ai';
import { createOpenAI } from '@ai-sdk/openai';
const gateway = 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: gateway('gpt-4o-mini'),
messages,
});
return result.toDataStreamResponse();
}
n4n.ai exposes one OpenAI-compatible endpoint that addresses 240+ models and automatically falls back when a provider is rate-limited or degraded, so the above works without per-vendor logic.
The runtime = 'edge' line enables low-latency streaming on Vercel’s edge runtime. On other hosts, use the Node runtime and remove that export.
Frontend: useChat hook
Replace app/page.tsx with a client component:
'use client';
import { useChat } from '@ai-sdk/react';
export default function Page() {
const { messages, input, handleInputChange, handleSubmit, isLoading } = useChat();
return (
<main style={{ maxWidth: 720, margin: '40px auto', fontFamily: 'sans-serif' }}>
<h1>Chat</h1>
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
{messages.map((m) => (
<div key={m.id} style={{ padding: 8, border: '1px solid #ddd', borderRadius: 6 }}>
<strong>{m.role}: </strong>
{m.content}
</div>
))}
{isLoading && <div>…</div>}
</div>
<form onSubmit={handleSubmit} style={{ marginTop: 16, display: 'flex', gap: 8 }}>
<input
value={input}
onChange={handleInputChange}
placeholder="Type a message"
style={{ flex: 1, padding: 8 }}
/>
<button type="submit" disabled={isLoading}>
Send
</button>
</form>
</main>
);
}
useChat owns message state, binds the input, and POSTs to /api/chat. It expects the route to return a data stream, which toDataStreamResponse() supplies.
Environment and run
Create .env.local:
N4N_API_KEY=sk-your-key-here
Start the dev server:
npm run dev
Open http://localhost:3000. You should see the heading and an empty message list.
Expected output at checkpoint
Type “Hello” and press Send. The UI updates to:
user: Hello
assistant: Hi there! How can I help you today?
Tokens stream in sequentially. The isLoading indicator clears when the generation finishes.
Understanding the data flow
The browser sends { messages: [...] } as JSON. useChat serializes the conversation history using the standard role/content shape. The route passes those messages to streamText, which calls the gateway’s /v1/chat/completions with stream: true. The SDK transforms the SSE chunks into a client-readable data stream.
No manual WebSocket or fetch polling is required.
Error handling and retries
Gateways can return 429 or 503 under load. The AI SDK exposes error from useChat. Extend the component:
const { messages, input, handleInputChange, handleSubmit, isLoading, error } = useChat();
{error && <div style={{ color: 'red' }}>Failed: {error.message}</div>}
Because the backend already switches providers on degradation, the client rarely needs custom retry logic.
Passing model and routing hints
To pin a model per request, send extra body fields from the client:
const { messages, input, handleInputChange, handleSubmit } = useChat({
body: { model: 'claude-3-5-sonnet' },
});
Read them in the route:
const { messages, model } = await req.json();
const result = await streamText({
model: gateway(model ?? 'gpt-4o-mini'),
messages,
});
The gateway honors client routing directives and forwards provider cache-control hints, so upstream caching behaves as the origin model intends.
Customizing message rendering
For markdown or code blocks, map m.content through a renderer. Keep the key on m.id to avoid React list warnings.
{messages.map((m) => (
<div key={m.id} className={m.role === 'user' ? 'user' : 'assistant'}>
<span className="role">{m.role}</span>
<div className="content">{m.content}</div>
</div>
))}
Add a CSS module or inline styles as needed. The hook does not constrain your markup.
Production considerations
- Set
export const maxDuration = 30;in the route for longer serverless timeouts. - Never import
process.env.N4N_API_KEYinto a client component. - Use
result.toDataStreamResponse({ getErrorMessage: () => 'Stream failed' })to mask internal errors.
Final check
You now have a runnable useChat Next.js chat app tutorial implementation with less than 60 lines of application code. The Vercel AI SDK handles the wire format; the gateway handles model routing and fallback. Swap the model string to access different capabilities without touching the UI.