← Course index

streamText in a route handler

A minimal API route that streams chat completions to the client.

Day 3: Streaming text·~16 min read

On this page

After this topic

You will have a Route Handler you can paste into a Next.js app: read messages, call streamText, return a UI stream. You will know why the last line is toUIMessageStreamResponse() and not res.json(), and why the key never leaves this file.

Server route

The route is thin: read messages, call streamText, return the UI stream. The API key never leaves this file.

Keep the route thin: read messages from the body, call streamText, return the UI stream. Compare this to yesterday’s generateText — same model helper, same messages, different last line.

This file runs on the server. That is the entire security model: the Gateway key is an env var here, never in the bundle. The browser only sees a fetch to /api/chat.

You do not need runtime = 'edge' to stream. Next.js on Vercel Fluid Compute (Node) streams fine. Edge is a later, optional constraint — not a requirement of streamText.

How a stream reaches the browser. Tokens leave the provider as they exist. Your Route Handler forwards them. useChat paints them. Nothing waits for the last token.
  1. 01

    This is a Next.js Route Handler

    POST /api/chat runs on the server (Fluid Compute / Node). You do not need runtime = 'edge' to stream.

  2. 02

    messages come from the client

    useChat sends the thread as JSON. Same shape as generateText({ messages }). Treat it as untrusted data — a user can POST anything.

  3. 03

    streamText instead of generateText

    The model still produces tokens one by one. We just do not wait for the last one before sending the first.

  4. 04

    toUIMessageStreamResponse()

    Turns the SDK stream into the HTTP response useChat knows how to read. res.json(text) here would throw away streaming. Older tutorials used toDataStreamResponse — that is the wrong last line for useChat in v6.

The browser half

useChat is the client. It posts the thread to /api/chat and updates messages as chunks arrive. You still do not put a key in this file. The snippet is abbreviated — the Streaming Chat catalog has a full UI.

sendMessage({ text }) is the v6 helper. You manage the input with useState. Older tutorials used handleSubmit, handleInputChange, and an api: option on useChat. Copy this shape, not a 2024 snippet.

status tells you whether a reply is in flight. 'ready' means you can send. Anything else, disable the input. Do not invent a second loading flag unless you need one.

  1. 01

    useChat is the client

    DefaultChatTransport POSTs to /api/chat — the streamText route above. The key stays on the server.

  2. 02

    sendMessage({ text })

    That is AI SDK v6. You own the input string. After send, you clear it. The hook owns the messages array.

  3. 03

    status, not a homemade spinner flag

    status !== 'ready' means a request is submitted or streaming. Disable the input so the user cannot double-send.

What this route does not do yet

It does not persist the thread. Refresh the tab and the chat is gone unless you save messages yourself. It does not cap output length. It does not call tools. Those are later lessons, not missing imports.

It also does not authenticate the user. /api/chat is a public POST until you add auth. Treat the body as hostile even after you do.

Common questions

What is “streamText in a route handler”?
A minimal API route that streams chat completions to the client.
What will I be able to do after this lesson?
You will have a Route Handler you can paste into a Next.js app: read messages, call streamText, return a UI stream. You will know why the last line is toUIMessageStreamResponse() and not res.json(), and why the key never leaves this file.
How long does this lesson take?
About 16 minutes of reading. It is a free chapter in the AI SDK Patterns TypeScript course.
When should I not use this?
It does not persist the thread. Refresh the tab and the chat is gone unless you save messages yourself. It does not cap output length. It does not call tools. Those are later lessons, not missing imports.

Written by Akash Panchal·Updated August 29, 2026