Building a responsive chat UI shouldn’t require wrestling with WebSocket plumbing. This hands-on tutorial implements a useChat hook GPT-4o streaming n4n.ai integration with the Vercel AI SDK, giving you token-by-token responses in a React component with under 50 lines of server code.
Prerequisites
- Node.js 18.18 or later (App Router relies on modern async request APIs)
- A Next.js 14+ project. If you don’t have one, scaffold it:
npx create-next-app@latest demo --ts --app - An API key from the gateway (set as
N4N_API_KEYin.env.local) - Basic familiarity with React hooks, TypeScript, and fetch
You do not need to install the official OpenAI SDK. The Vercel AI SDK speaks the OpenAI chat protocol against any compatible base URL.
Project setup
Install the required packages:
npm install ai @ai-sdk/openai @ai-sdk/react zod
@ai-sdk/openai provides the provider factory. @ai-sdk/react exposes useChat. ai ships streamText and the data stream protocol helpers.
Create .env.local at the project root:
N4N_API_KEY=sk-your-key-here
Your file tree should include:
app/
api/chat/route.ts
components/chat.tsx
page.tsx
Server route: streaming with GPT-4o
Building the route handler
The route receives messages from the client and returns a streaming response. We point the OpenAI-compatible provider at the gateway endpoint.
// app/api/chat/route.ts
import { streamText } from 'ai';
import { createOpenAI } from '@ai-sdk/openai';
const n4nProvider = createOpenAI({
baseURL: 'https://api.n4n.ai/v1',
apiKey: process.env.N4N_API_KEY,
});
export async function POST(req: Request) {
const { messages } = await req.json();
const result = await streamText({
model: n4nProvider('gpt-4o'),
messages,
});
return result.toDataStreamResponse();
}
toDataStreamResponse() serializes the stream into the Vercel AI SDK data stream format. The useChat hook parses this format natively—no custom event listeners required.
Stream protocol details
The wire format is newline-delimited data: frames. A text delta looks like:
data: {"type":"text","value":"The"}
data: {"type":"text","value":" quick"}
The stream terminates with data: [DONE].
Expected output from curl
Verify the route 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 5 words"}]}'
You should see incremental data: frames and a final [DONE]. If you get a 401, check the key. A 400 means malformed messages.
Client: useChat hook
Wire up the provider
Create a client component. The hook manages messages, input, submission, and loading state.
// app/components/chat.tsx
'use client';
import { useChat } from '@ai-sdk/react';
export default function Chat() {
const { messages, input, handleInputChange, handleSubmit, isLoading, stop } =
useChat({ api: '/api/chat' });
return (
<div style={{ maxWidth: 600, margin: '2rem auto' }}>
{messages.map((m) => (
<div key={m.id} style={{ margin: '1rem 0' }}>
<strong>{m.role}:</strong> {m.content}
</div>
))}
<form onSubmit={handleSubmit}>
<input
value={input}
onChange={handleInputChange}
placeholder="Type a message…"
style={{ width: '80%' }}
/>
<button type="submit" disabled={isLoading}>
Send
</button>
{isLoading && (
<button type="button" onClick={() => stop()} style={{ marginLeft: 8 }}>
Stop
</button>
)}
</form>
</div>
);
}
The chat component
Mount it in a page:
// app/page.tsx
import Chat from './components/chat';
export default function Page() {
return (
<main>
<h1>GPT-4o Streaming Demo</h1>
<Chat />
</main>
);
}
The useChat hook GPT-4o streaming pattern keeps the server stateless; the client sends the full messages array on every request. For prototypes and low-latency internal tools this is perfectly fine.
Running and verifying
Checkpoint: first message
Start the dev server:
npm run dev
Open http://localhost:3000. Type “Explain TCP fast open”. The assistant message renders incrementally:
user: Explain TCP fast open
assistant: TCP Fast Open (TFO) allows data to be sent in the initial SYN packet…
In the browser network tab, filter for chat and inspect the response. You’ll see the data: frames arriving every few milliseconds. React re-renders only the changing message node.
Adding a system prompt
Most real apps need a system message. Prepend it server-side so the client can’t mutate it:
// inside route.ts POST
const { messages } = await req.json();
const result = await streamText({
model: n4nProvider('gpt-4o'),
system: 'You are a concise senior network engineer.',
messages,
});
Because the gateway is OpenAI-compatible, system maps to the standard top-level instruction field.
Extending the integration
Temperature and max tokens
Pass standard sampling params:
const result = await streamText({
model: n4nProvider('gpt-4o'),
messages,
temperature: 0.2,
maxTokens: 512,
});
Why this pattern scales
The client remains dumb. All model selection, key custody, and prompt engineering live in the route. Swapping gpt-4o for another model ID is a one-line change. The useChat hook handles reconnection, message IDs, and optimistic UI for free.
Production notes
Keep N4N_API_KEY server-side only. If you deploy to Vercel Edge, add export const runtime = 'edge' to route.ts for lower tail latency, but the Node runtime works identically because the gateway is a plain HTTPS API.
For long conversations, prune messages before calling streamText to control token spend. The gateway meters per token; you pay for exactly what streams.
Use stop() on the client to abort mid-stream; the route’s streamText honors the AbortSignal from the incoming request, so the upstream connection is cancelled promptly.