---
title: "streamText in a route handler"
summary: "A minimal API route that streams chat completions to the client."
track: "The model"
day: 3
minutes: 16
author: "Akash Panchal"
url: https://ai-sdk-patterns.dev/learn/fundamentals/day-3/stream-text-api
dateModified: 2026-08-29
---

# streamText in a route handler

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

*The model · Day 3: Streaming text · ~16 min. Written by [Akash Panchal](https://github.com/akashp1712).*

Canonical: https://ai-sdk-patterns.dev/learn/fundamentals/day-3/stream-text-api

## 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.

### app/api/chat/route.ts

```ts
import { streamText } from "ai";
import { getModel } from "@/lib/model";

export async function POST(req: Request) {
  const { messages } = await req.json();

  const result = streamText({
    model: getModel(),
    messages,
  });

  return result.toUIMessageStreamResponse();
}
```

- **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.
- **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.
- **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.
- **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.

### components/chat.tsx

```ts
"use client";

import { useChat } from "@ai-sdk/react";
import { DefaultChatTransport } from "ai";
import { useState } from "react";

export function Chat() {
  const [input, setInput] = useState("");
  const { messages, sendMessage, status } = useChat({
    transport: new DefaultChatTransport({ api: "/api/chat" }),
  });

  const busy = status !== "ready";

  return (
    <form
      onSubmit={(e) => {
        e.preventDefault();
        sendMessage({ text: input });
        setInput("");
      }}
    >
      <input value={input} onChange={(e) => setInput(e.target.value)} disabled={busy} />
    </form>
  );
}
```

- **useChat is the client.** DefaultChatTransport POSTs to /api/chat — the streamText route above. The key stays on the server.
- **sendMessage({ text }).** That is AI SDK v6. You own the input string. After send, you clear it. The hook owns the messages array.
- **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.
