← Blog

Claude API Streaming Node.js Example

2026-10-09 · 4 min read · SubToAPI Team

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

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.

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 →