← Blog

Claude API Streaming Response in Next.js: Full Example

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

Streaming a Claude API response in Next.js means setting up a route handler that proxies the model's token-by-token output to the browser, then consuming that stream on the client so text appears incrementally instead of all at once. This pattern is what you want for chat UIs, writing assistants, or any interface where users shouldn't stare at a blank screen while the model generates a full answer.

Below is a working example using the Next.js App Router, with a route handler that streams Server-Sent Events and a client component that reads the stream with the Fetch API. The same pattern applies whether you're calling Claude directly or going through a proxy like SubToAPI — the streaming mechanics on the Next.js side are identical.

Why streaming matters in Next.js

Without streaming, your API route waits for the full completion before returning a response. For long outputs, that can mean 10-30 seconds of silence followed by a wall of text. Streaming fixes the perceived latency problem: the first token can arrive in under a second, and the rest trickles in as it's generated.

Next.js supports this natively because Route Handlers can return a ReadableStream as the response body. You don't need a separate WebSocket server or third-party streaming library — the Web Streams API built into the Edge and Node runtimes handles it.

Setting up the route handler

Create a route handler that calls the Claude API with "stream": true and forwards the chunks:

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

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

  const upstream = await fetch("https://api.anthropic.com/v1/messages", {
    method: "POST",
    headers: {
      "x-api-key": process.env.ANTHROPIC_API_KEY,
      "anthropic-version": "2023-06-01",
      "content-type": "application/json",
    },
    body: JSON.stringify({
      model: "claude-sonnet-4-5",
      max_tokens: 1024,
      stream: true,
      messages,
    }),
  });

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

This forwards the upstream SSE stream directly to the client without buffering it in memory. The key detail is runtime = "edge" — edge functions handle streaming responses more reliably than some serverless Node configurations, which can buffer output and defeat the purpose of streaming.

If you're using SubToAPI instead of calling Anthropic directly, the route handler looks almost the same, just pointed at a different endpoint and header:

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

Full request and response shapes are in the streaming docs.

Parsing SSE events on the server side

Claude's streaming format sends a sequence of named events (message_start, content_block_delta, message_stop, etc.), each as a JSON payload prefixed with data: . If you want to transform or filter the stream before sending it to the client — say, extracting only text deltas — pipe it through a TransformStream:

function createTextExtractor() {
  const decoder = new TextDecoder();
  let buffer = "";

  return new TransformStream({
    transform(chunk, controller) {
      buffer += decoder.decode(chunk, { stream: true });
      const lines = buffer.split("\n");
      buffer = lines.pop();

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

        const event = JSON.parse(payload);
        if (event.type === "content_block_delta") {
          const text = event.delta?.text ?? "";
          controller.enqueue(new TextEncoder().encode(text));
        }
      }
    },
  });
}

Then pipe the upstream body through it:

const stream = upstream.body.pipeThrough(createTextExtractor());
return new Response(stream, {
  headers: { "Content-Type": "text/plain; charset=utf-8" },
});

This simplifies the client — it just receives raw text chunks instead of having to parse SSE event types itself.

Consuming the stream on the client

In a React client component, use fetch with a ReadableStream reader and update state as chunks arrive:

"use client";
import { useState } from "react";

export default function Chat() {
  const [output, setOutput] = useState("");

  async function sendMessage(text) {
    setOutput("");
    const res = await fetch("/api/chat", {
      method: "POST",
      body: JSON.stringify({
        messages: [{ role: "user", content: text }],
      }),
    });

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

    while (true) {
      const { done, value } = await reader.read();
      if (done) break;
      setOutput((prev) => prev + decoder.decode(value));
    }
  }

  return (
    <div>
      <button onClick={() => sendMessage("Explain streaming in one paragraph")}>
        Ask Claude
      </button>
      <p>{output}</p>
    </div>
  );
}

This is the minimal version. In production you'd also handle abort signals (so navigating away cancels the fetch), error states if the upstream returns a non-200 status, and probably render markdown as it streams in rather than plain text.

Common pitfalls

A few things that break streaming in Next.js specifically:

If you'd rather not run your own Claude account management, token handling, and billing logic, SubToAPI gives you the same stream: true interface over HTTPS with per-key usage tracking, so the Next.js code above works unchanged — just swap the base URL and auth header. Get a key by signing up and check pricing for Solo, Team, and Scale plans.

Questions

Does Next.js support streaming by default, or do I need middleware? No middleware is needed. Route Handlers can return a ReadableStream natively, and the App Router's edge runtime is built to handle streamed responses without buffering.

Why does my stream arrive all at once instead of incrementally? This almost always means something in the chain is buffering — either the server runtime (switch to runtime = "edge"), a reverse proxy without the right cache headers, or client code calling .text() instead of reading the stream chunk by chunk.

Can I use the same streaming setup with SubToAPI instead of the Anthropic API directly? Yes. The request and SSE response format match Claude's native streaming, so you only change the base URL to https://api.subtoapi.app/v1/messages and the auth header, as documented in the quickstart.

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 →