Claude API Streaming Node.js Example
If you're looking for a Claude API streaming Node.js example, you want working code that opens a stream, reads tokens as they arrive, and prints or forwards them without waiting for the full response. That's exactly what this article gives you: a minimal fetch-based example, a version using an SDK-style client, and the common pitfalls that break streaming in Node (buffering, chunk splitting, and timeout handling).
Streaming matters for any app with a chat interface or long-form generation — users see output immediately instead of staring at a spinner for 10-30 seconds. In Node.js, streaming means reading a ReadableStream (or Node stream) of Server-Sent Events (SSE) and parsing each data: line as it arrives, rather than awaiting the entire JSON body.
How Claude API streaming works
Claude's Messages API supports streaming by setting "stream": true in the request body. Instead of one JSON response, the server sends a sequence of SSE events: message_start, multiple content_block_delta events (each containing a token chunk), and finally message_stop. Your client reads the HTTP response body as a stream and processes events as they arrive.
In Node.js this is straightforward with native fetch (Node 18+) since the response body is a web-standard ReadableStream. You don't need a special library to consume it — you need a loop that reads chunks, splits them on newlines, and extracts the JSON payload from each data: line.
Minimal Node.js streaming example
Here's a self-contained example using native fetch and no dependencies:
async function streamMessage(apiKey, prompt) {
const response = await fetch('https://api.anthropic.com/v1/messages', {
method: 'POST',
headers: {
'content-type': 'application/json',
'x-api-key': apiKey,
'anthropic-version': '2023-06-01',
},
body: JSON.stringify({
model: 'claude-3-5-sonnet-20241022',
max_tokens: 1024,
stream: true,
messages: [{ role: 'user', content: prompt }],
}),
});
if (!response.ok) {
throw new Error(`Request failed: ${response.status}`);
}
const reader = response.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(); // keep incomplete line for next chunk
for (const line of lines) {
if (!line.startsWith('data: ')) continue;
const payload = line.slice(6);
if (payload === '[DONE]') return;
const event = JSON.parse(payload);
if (event.type === 'content_block_delta') {
process.stdout.write(event.delta.text ?? '');
}
}
}
}
streamMessage(process.env.ANTHROPIC_API_KEY, 'Explain event loops in one paragraph.');
This handles the most common bug in hand-rolled SSE parsers: chunks don't align with event boundaries. A single data: line can arrive split across two read() calls, so you must buffer incomplete lines instead of parsing each chunk independently.
Streaming to a browser client via Node backend
Most real apps don't stream directly to the terminal — they proxy the stream from a Node backend (Express, Fastify, Next.js API route) to a browser using SSE or a readable response. The pattern is the same: read Claude's stream, re-emit each delta to your own client connection.
app.post('/chat', async (req, res) => {
res.setHeader('Content-Type', 'text/event-stream');
res.setHeader('Cache-Control', 'no-cache');
const upstream = await fetch('https://api.anthropic.com/v1/messages', {
method: 'POST',
headers: {
'content-type': 'application/json',
'x-api-key': process.env.ANTHROPIC_API_KEY,
'anthropic-version': '2023-06-01',
},
body: JSON.stringify({
model: 'claude-3-5-sonnet-20241022',
max_tokens: 1024,
stream: true,
messages: req.body.messages,
}),
});
const reader = upstream.body.getReader();
const decoder = new TextDecoder();
while (true) {
const { done, value } = await reader.read();
if (done) break;
res.write(decoder.decode(value, { stream: true }));
}
res.end();
});
This keeps your frontend decoupled from Anthropic's exact event schema if you parse and reshape events before forwarding them — useful if you later swap providers or add logging.
Using SubToAPI instead of raw Anthropic streaming
Writing SSE parsing by hand works, but once you have multiple services calling Claude — a web app, a Slack bot, a background worker — you end up duplicating this parsing logic everywhere, plus you have to manage raw API keys and manually track usage per service.
SubToAPI turns your existing Claude access into a standard HTTPS API with sub_live_... application keys, so each service gets its own key and you keep one dashboard for usage and billing. Streaming works the same way — you flip stream: true and consume SSE events exactly as shown above, just pointed at https://api.subtoapi.app/v1/messages:
curl https://api.subtoapi.app/v1/messages \
-H "Authorization: Bearer $SUBTOAPI_KEY" \
-H "content-type: application/json" \
-d '{
"model": "claude-3-5-sonnet-20241022",
"max_tokens": 1024,
"stream": true,
"messages": [{"role": "user", "content": "Explain event loops in one paragraph."}]
}'
The Node.js example above works unchanged — just swap the URL and header. See the streaming docs and quickstart for the full request/response reference, including tool use during streaming (docs/tools) and the full message format (docs/messages). Plans start at €9/month on the Solo tier, with team seats on Team (€19/seat) and Scale (€49/seat) — see pricing or start a free trial at signup.
Common streaming bugs in Node.js
- Parsing chunks independently: always buffer across
read()calls and split on newlines, not per-chunk. - Forgetting
anthropic-version: Claude's API requires this header on every request, streaming or not. - Not handling
message_stop: some clients loop forever if they don't break on the terminal event. - Timeouts on long streams: default HTTP client timeouts (especially behind proxies like Nginx or Vercel) can cut a stream short — disable buffering and raise timeouts for streaming routes specifically.
questions
Does Claude's streaming API require a special Node.js library? No. Native fetch in Node 18+ exposes a web-standard ReadableStream, which is enough to consume SSE events without any third-party package.
Why does my stream get cut off midway? Usually a proxy or server timeout (Nginx, load balancer, serverless function limit) closing the connection before message_stop arrives. Increase the timeout or disable response buffering for that route.
Can I stream Claude responses through SubToAPI the same way as the native API? Yes — set "stream": true and parse SSE events identically; only the base URL and Authorization: Bearer header change. See docs/streaming for details.