Claude API Streaming Response in Next.js: Full Example
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:
- Using the Node runtime with response buffering enabled. Some hosting setups buffer the entire response before sending it, which defeats streaming. The edge runtime avoids this.
- Forgetting
Cache-Control: no-cache. Some CDNs and proxies will try to cache or buffer streaming responses without this header. - Awaiting
upstream.text()or.json()instead of passingupstream.bodythrough. That forces Next.js to wait for the entire response before it can start sending anything to the client. - Not handling
stream: trueon the request body. If this is missing, the Claude API returns the full completion as a single JSON object and there's nothing to stream.
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.