How do you stream Claude responses in Next.js?

You stream Claude responses in Next.js by calling the Claude API from a route handler on the server and returning its output as a streamed Response. The Anthropic TypeScript SDK's client.messages.stream() produces the reply piece by piece; you push each piece into a ReadableStream; the browser reads that stream and paints the text as it arrives.

This keeps the API key on the server, works on any host that supports streaming responses, and needs no extra libraries beyond the official SDK.

What do you need before you start?

You need a Next.js project that uses the App Router, a Claude API key, and the SDK:

npm install @anthropic-ai/sdk

Add the key to .env.local as ANTHROPIC_API_KEY=.... The SDK reads that variable automatically. Never prefix it with NEXT_PUBLIC_, because that would bundle it into the browser code.

How do you write the streaming route handler?

Create app/api/chat/route.ts. The handler validates the incoming conversation, opens a stream to Claude and forwards every text delta to the browser.

import Anthropic from "@anthropic-ai/sdk";

const client = new Anthropic(); // reads ANTHROPIC_API_KEY
const MAX_MESSAGES = 30;
const MAX_CHARS = 4000;

export async function POST(req: Request) {
  const { messages } = (await req.json()) as { messages: Anthropic.MessageParam[] };

  if (!Array.isArray(messages) || messages.length === 0 || messages.length > MAX_MESSAGES) {
    return new Response("Invalid conversation", { status: 400 });
  }
  for (const m of messages) {
    if (typeof m.content !== "string" || m.content.length > MAX_CHARS) {
      return new Response("Message too long", { status: 400 });
    }
  }

  const stream = client.messages.stream({
    model: "claude-opus-5",
    max_tokens: 64000,
    system: "You are a helpful assistant for our product. Answer briefly and clearly.",
    messages,
  });

  const encoder = new TextEncoder();
  const body = new ReadableStream<Uint8Array>({
    async start(controller) {
      try {
        for await (const event of stream) {
          if (event.type === "content_block_delta" && event.delta.type === "text_delta") {
            controller.enqueue(encoder.encode(event.delta.text));
          }
        }
        controller.close();
      } catch (err) {
        controller.error(err);
      }
    },
    cancel() {
      stream.abort(); // the user closed the page or pressed stop
    },
  });

  return new Response(body, {
    headers: { "Content-Type": "text/plain; charset=utf-8", "Cache-Control": "no-cache" },
  });
}

A few choices here are deliberate:

  • Streaming with a high max_tokens. Long replies do not hit HTTP timeouts when streamed, so the limit can be generous.
  • Only text_delta events are forwarded. The stream also carries events such as message_start, content_block_stop and message_delta; the browser does not need them for a plain chat.
  • cancel() aborts the Claude stream. If the visitor leaves, you stop paying for tokens nobody will read.
  • The model ID is the current default Claude model at the time of writing (September 2026). Check Anthropic's model list when you build.

How does the browser read the stream?

The browser sends the whole conversation, then reads the response body chunk by chunk. Put this in a client component, for example app/chat/Chat.tsx.

"use client";

import { useState } from "react";

type Msg = { role: "user" | "assistant"; content: string };

export default function Chat() {
  const [messages, setMessages] = useState<Msg[]>([]);
  const [input, setInput] = useState("");
  const [busy, setBusy] = useState(false);

  async function send() {
    const history: Msg[] = [...messages, { role: "user", content: input }];
    setMessages([...history, { role: "assistant", content: "" }]);
    setInput("");
    setBusy(true);

    const res = await fetch("/api/chat", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ messages: history }),
    });
    if (!res.ok || !res.body) throw new Error(`Chat request failed: ${res.status}`);

    const reader = res.body.getReader();
    const decoder = new TextDecoder();
    while (true) {
      const { done, value } = await reader.read();
      if (done) break;
      const chunk = decoder.decode(value, { stream: true });
      setMessages((prev) => {
        const last = prev[prev.length - 1];
        return [...prev.slice(0, -1), { ...last, content: last.content + chunk }];
      });
    }
    setBusy(false);
  }

  return (
    <div>
      {messages.map((m, i) => (
        <p key={i}><strong>{m.role === "user" ? "You" : "Claude"}:</strong> {m.content}</p>
      ))}
      <input value={input} onChange={(e) => setInput(e.target.value)} disabled={busy} />
      <button onClick={send} disabled={busy || !input.trim()}>Send</button>
    </div>
  );
}

The key line is decoder.decode(value, { stream: true }). The stream: true flag stops multi-byte characters, such as accented letters or emoji, from breaking when they are split across two chunks.

Why does the conversation history go with every request?

The Claude Messages API is stateless: it does not remember earlier requests. Each call must include the full list of user and assistant turns so far. The component above keeps that list in React state and sends it every time.

For long chats, trim the history on the server (for example, keep the last 30 turns) or summarize older turns. Otherwise every request gets bigger, slower and more expensive.

How do you make the chat feel fast?

Streaming solves most of the waiting problem, but a few settings matter too:

Problem Cause Fix
Long pause before the first word The model is thinking before it writes Lower the effort setting (output_config: { effort: "low" }) for simple chat replies
Text arrives all at once A proxy or host buffers the response Check that your host supports streamed responses; avoid middleware that reads the whole body
Stream stops on long answers The host's function time limit Raise the route's maximum duration on your host, or keep answers shorter
Costs grow over a long chat Full history is resent every turn Trim or summarize old turns; use prompt caching for a long, fixed system prompt

Recent Claude models use adaptive thinking by default, which improves hard answers but adds a short pause first. For a quick support chat, a lower effort level is usually the better trade.

How do you keep a streaming chat secure?

A chat endpoint is a public door to a paid API, so protect it:

  • Keep the key on the server and never log it.
  • Validate input: the handler above rejects empty, huge or malformed conversations.
  • Add rate limiting per user or IP address before launch.
  • Put your rules in the system prompt, not in user messages, and do not trust the role values the browser sends without checking them.
  • Require sign-in if the chat can reach private data or tools.

Summary

Streaming Claude in Next.js takes two small pieces: a route handler that turns client.messages.stream() into a ReadableStream, and a client component that reads it with getReader(). Add input limits, stream cancellation and rate limiting, and you have a chat that feels fast and stays safe. To add tools to the same chat, see building an agent with tool use. For a full product built this way, see Next.js web apps.

Need this built? See Next.js web apps or get in touch.

FAQ

Questions about this topic

Why stream the response instead of waiting for the full answer?

A long answer can take many seconds to generate. Streaming shows the first words almost at once, so the chat feels fast even when the full reply takes a while.

Can I call the Claude API directly from a React component?

No. Doing that would expose your API key to every visitor. Keep the call in a route handler or server action and let the browser talk only to your own endpoint.

Do I need Server-Sent Events for this?

Not for plain text. Returning a ReadableStream of text and reading it with getReader() is enough. Use Server-Sent Events or JSON lines when you also need to send structured events such as tool calls or usage data.

Does this work with the Pages Router?

The same idea works in a Pages Router API route, but the App Router's route handlers return a standard Response with a ReadableStream, which makes streaming simpler.

Keep reading

More on Next.js web apps

Have a bot, a backend or a strategy in mind?

Tell me what you want to build and where you are with it. Send a few lines about the project and I’ll reply with questions and next steps.

Rajshahi, Bangladesh