Route handler and useChat with Zod schemas
The agent starts as a plain streaming chat. Browser sends messages to a POST route, the route calls streamText, and the response streams back as UI messages. Once this baseline works, every other piece we add (tools, reasoning, memory) plugs into this same stream.
import { streamText, convertToModelMessages } from 'ai';
import { createOpenRouter } from '@openrouter/ai-sdk-provider';
import { SYSTEM_PROMPT } from '@/lib/utils';
export const maxDuration = 60;
export async function POST(req: Request) {
const { messages } = await req.json();
const openrouter = createOpenRouter({
apiKey: process.env.OPENROUTER_API_KEY ?? '',
});
const result = streamText({
model: openrouter.chat(
process.env.OPENROUTER_MODEL ?? 'google/gemma-3-12b-it',
),
system: SYSTEM_PROMPT,
messages: convertToModelMessages(messages),
});
return result.toUIMessageStreamResponse();
}streamText opens a model stream. toUIMessageStreamResponse turns it into the exact protocol useChat understands on the client. No custom parsing needed.
'use client';
import { useChat } from '@ai-sdk/react';
import { useState } from 'react';
export function ChatPanel() {
const [input, setInput] = useState('');
const { messages, sendMessage, status } = useChat({
api: '/api/chat',
});
const isLoading = status === 'submitted' || status === 'streaming';
return (
<form
onSubmit={(e) => {
e.preventDefault();
if (!input.trim()) return;
sendMessage({ text: input });
setInput('');
}}
>
{messages.map((m) => (
<div key={m.id}>
<strong>{m.role}:</strong> {JSON.stringify(m.parts)}
</div>
))}
<input value={input} onChange={(e) => setInput(e.target.value)} />
<button type="submit" disabled={isLoading}>Send</button>
</form>
);
}useChat gives you messages, sendMessage, and a status flag. Each message has a parts array so text and tool invocations can sit inside one message. We will render them properly once tools arrive.
import { z } from 'zod';
// The request body the route handler accepts from useChat
export const chatRequestSchema = z.object({
messages: z.array(
z.object({
id: z.string(),
role: z.enum(['user', 'assistant', 'system']),
parts: z.array(z.object({ type: z.string() }).passthrough()),
}),
),
});
export type ChatRequest = z.infer<typeof chatRequestSchema>;Zod is the shared contract between client and server. Parse req.json() with this schema and malformed payloads fail before they reach the model.
Quiz: Quiz
Loading practiceโฆ