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:
On the frontend we use useChat() to manage message state and send new input. Submitting the form sends the text and clears the input:
The UI components render each message’s role and concatenated text parts, and provide a styled input:
The dev server is started from main.ts with the folder set as the root:
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.
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,});};
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>);};
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>);};
import { runLocalDevServer } from '#shared/run-local-dev-server.ts';await runLocalDevServer({root: import.meta.dirname,});