← Blog

Claude API Streaming Response in a Next.js App

2026-10-17 · 5 min read · SubToAPI Team

If you're building a chat interface in Next.js and want text to appear token-by-token instead of waiting for a full response, you need to stream the Claude API response through your backend and into your frontend. This means setting stream: true on your API request, handling a server-sent-events-style response in a Next.js Route Handler, and reading that stream on the client with the Fetch API or EventSource.

The short version: Claude's API returns a stream of data: events when you ask for one. Your Next.js app acts as a proxy — it calls Claude (or a gateway like SubToAPI), gets back a ReadableStream, and forwards that stream unchanged to the browser. The browser then reads chunks as they arrive and updates the UI. No polling, no websockets, just HTTP streaming. Below is a working setup you can drop into an App Router project.

Why stream at all

Without streaming, your API route waits for the full completion before responding. For a 500-token answer, that's several seconds of a blank screen. Streaming starts rendering text within a few hundred milliseconds and keeps the user engaged while the model is still generating — this is the difference between a chatbot that feels responsive and one that feels broken.

Setting up the Route Handler

In the App Router, a streaming proxy lives in app/api/chat/route.ts. The key is forwarding the upstream ReadableStream directly instead of buffering it into a string:

// app/api/chat/route.ts
export const runtime = "edge";

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

  const upstream = await fetch("https://api.subtoapi.app/v1/messages", {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      Authorization: `Bearer ${process.env.SUBTOAPI_KEY}`,
    },
    body: JSON.stringify({
      model: "claude-sonnet-4-5",
      max_tokens: 1024,
      stream: true,
      messages,
    }),
  });

  if (!upstream.ok || !upstream.body) {
    return new Response("Upstream error", { status: 502 });
  }

  return new Response(upstream.body, {
    headers: {
      "Content-Type": "text/event-stream",
      "Cache-Control": "no-cache",
      Connection: "keep-alive",
    },
  });
}

Using runtime = "edge" avoids Node's default body buffering and keeps the connection open for as long as the model is generating. If you're on Node runtime (e.g. because you need a larger timeout or other Node APIs), the same pattern works — just make sure your hosting provider doesn't buffer SSE responses behind a reverse proxy.

Point this at Claude directly if you already manage your own Anthropic key, or at SubToAPI's /v1/messages endpoint if you want a single HTTPS key, usage metadata, and team-level access control without wiring up Anthropic's SDK yourself. The request and response shapes are the same either way — see /docs/messages and /docs/streaming for the full event reference.

Consuming the stream on the client

On the frontend, read the response body with the native ReadableStreamDefaultReader and parse each data: line as it arrives:

async function sendMessage(messages, onToken) {
  const res = await fetch("/api/chat", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ messages }),
  });

  const reader = res.body.getReader();
  const decoder = new TextDecoder();
  let buffer = "";

  while (true) {
    const { done, value } = await reader.read();
    if (done) break;

    buffer += decoder.decode(value, { stream: true });
    const lines = buffer.split("\n");
    buffer = lines.pop();

    for (const line of lines) {
      if (!line.startsWith("data:")) continue;
      const payload = line.slice(5).trim();
      if (payload === "[DONE]") return;

      const event = JSON.parse(payload);
      if (event.type === "content_block_delta") {
        onToken(event.delta.text);
      }
    }
  }
}

Wire onToken into your component state and append each chunk to the message being rendered. This gives you the typing effect users expect from a modern chat UI, and it works the same way whether you're hitting Claude's API directly or going through a gateway — the SSE event types (message_start, content_block_delta, message_stop, etc.) are documented at /docs/streaming.

Handling tool use and errors mid-stream

Streaming responses that involve tool calls emit content_block_start and content_block_delta events for the tool input JSON before the model stops to wait for a tool result. If your app uses tools, check event.type === "content_block_start" and event.content_block.type === "tool_use" to detect when the model wants to call a function, and buffer the incremental JSON until the block closes. Details and examples are in /docs/tools.

Also handle the case where the upstream connection drops mid-stream — browsers won't retry SSE over fetch automatically. Wrap the reader loop in a try/catch, and on failure, show a "retry" affordance rather than silently truncating the message.

A note on rate limits and keys

If you're calling Claude directly, you're managing your own Anthropic key, rate limits, and billing per key. If your Next.js app has multiple environments (staging, preview deployments, local dev) or multiple team members hitting the API, that gets messy fast — shared keys end up in .env.local files that drift out of sync. SubToAPI gives each environment its own sub_live_... key, with usage visible per key in one dashboard, which keeps a multi-environment Next.js project from turning into a key-management headache. Sign up at /signup or check /docs/quickstart for the five-minute setup.

questions

Does streaming work with Next.js's Edge Runtime? Yes, and it's the recommended runtime for this use case. Edge Runtime doesn't buffer response bodies the way some Node.js setups do, so chunks reach the client as soon as they're forwarded. Node runtime also works, but confirm your hosting platform isn't buffering SSE behind a proxy.

Why does my stream arrive all at once instead of token-by-token? This is almost always a buffering issue outside your code — either a CDN/proxy in front of your Next.js app buffering the response, or Content-Encoding: gzip being applied to an SSE stream. Disable compression for the streaming route and check your hosting provider's docs on SSE support.

Can I use the Vercel AI SDK instead of hand-rolling this? Yes — the AI SDK's streamText helper wraps the same fetch-and-parse logic shown above and is a reasonable choice if you want built-in React hooks for message state. The raw approach here is useful when you need full control over tool-use events or are proxying through a custom gateway like SubToAPI.

Turn your Claude access into an HTTPS API

SubToAPI gives you application API keys, streaming, tool use and usage insights on top of your existing Claude access — set up in minutes.

Start free  Read the quickstart →