We’re running the solution to verify streaming text to the UI. Ask:
What is the capital of France?
The AI responds: Paris. Following up with:
How about Germany?
The AI maintains context and replies: Berlin. Now let’s look at the POST route to understand the solution.
Here we extract UI messages from the request body, convert them to model messages, stream text, then convert to a UI message stream and return a UI message stream response:
export const POST = async (req: Request): Promise<Response> => {const body = await req.json();const messages: UIMessage[] = body.messages;const modelMessages: ModelMessage[] =convertToModelMessages(messages);const streamTextResult = streamText({model: google('gemini-2.0-flash'),messages: modelMessages,});const stream = streamTextResult.toUIMessageStream();return createUIMessageStreamResponse({stream,});};
On the frontend we use useChat()
to manage message state and send new input. Submitting the form sends the text and clears the input:
const App = () => {const { messages, sendMessage } = useChat();const [input, setInput] = useState(`What's the capital of France?`,);return (<Wrapper>{messages.map((message) => (<Messagekey={message.id}role={message.role}parts={message.parts}/>))}<ChatInputinput={input}onChange={(e) => setInput(e.target.value)}onSubmit={(e) => {e.preventDefault();sendMessage({text: input,});setInput('');}}/></Wrapper>);};
The UI components render each message’s role and concatenated text parts, and provide a styled input:
export const Message = ({role,parts,}: {role: string;parts: UIMessagePart<UIDataTypes, UITools>[];}) => {const prefix = role === 'user' ? 'User: ' : 'AI: ';const text = parts.map((part) => {if (part.type === 'text') {return part.text;}return '';}).join('');return (<div className="prose prose-invert my-6"><ReactMarkdown>{prefix + text}</ReactMarkdown></div>);};
The dev server is started from main.ts
with the folder set as the root:
import { runLocalDevServer } from '#shared/run-local-dev-server.ts';await runLocalDevServer({root: import.meta.dirname,});
When you send a follow-up like:
What about Spain?
the entire message history is sent, and you’ll see the UIMessageStream
events streaming down to the browser, matching the structure you saw earlier in the terminal. This simple setup—useChat
on the frontend and a streamText
call on the backend returning a single UI message stream—will carry forward through the rest of the course.