← Blog

Claude API Streaming Response: TypeScript Example

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

Streaming a Claude API response in TypeScript means reading a Server-Sent Events (SSE) stream chunk by chunk instead of waiting for the full completion, then decoding each chunk into text as it arrives. This guide shows exactly how to do that: parsing the raw stream manually with fetch, consuming it with an async generator, and handling the specific event types Claude sends (content_block_delta, message_stop, etc.).

If you just want working code, skip to the "Minimal streaming client" section below. If you want to understand why streaming responses look the way they do and how to type them properly, keep reading.

Why streaming matters for TypeScript apps

A non-streaming request to a chat model blocks until the entire response is generated. For long answers — code generation, multi-step reasoning, long explanations — that can mean several seconds of silence before anything shows up in your UI. Streaming solves this by sending the response as a sequence of small events over an open HTTP connection, so you can render tokens as they're produced.

In TypeScript, this requires three things:

The shape of a streamed response

Claude's streaming API sends a series of named events over the connection. The ones you'll actually handle in a chat UI are:

Each event arrives as a line prefixed with event: followed by a data: line containing JSON.

Minimal streaming client

Here's a self-contained TypeScript function that opens a streaming request and yields text chunks as an async generator. This works in Node 18+, Deno, Bun, and browsers — anywhere fetch and ReadableStream are available.

type StreamEvent = {
  type: string;
  delta?: { type: string; text?: string };
  message?: { id: string; usage?: Record<string, number> };
};

async function* streamMessage(
  apiUrl: string,
  apiKey: string,
  body: Record<string, unknown>
): AsyncGenerator<string> {
  const res = await fetch(apiUrl, {
    method: "POST",
    headers: {
      "Authorization": `Bearer ${apiKey}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({ ...body, stream: true }),
  });

  if (!res.body) throw new Error("No response body");

  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 jsonStr = line.slice(5).trim();
      if (!jsonStr || jsonStr === "[DONE]") continue;

      const event: StreamEvent = JSON.parse(jsonStr);

      if (event.type === "content_block_delta" && event.delta?.text) {
        yield event.delta.text;
      }
    }
  }
}

Usage looks like this:

for await (const chunk of streamMessage(
  "https://api.subtoapi.app/v1/messages",
  process.env.SUBTOAPI_KEY!,
  {
    model: "claude-sonnet-4",
    max_tokens: 1024,
    messages: [{ role: "user", content: "Explain async generators in TypeScript" }],
  }
)) {
  process.stdout.write(chunk);
}

This pattern — buffer, split on newlines, parse data: lines, yield text deltas — is the core of every SSE consumer regardless of which provider you're calling. If you're routing requests through SubToAPI (https://subtoapi.app), the endpoint and auth header are the only things that change; the event format and parsing logic stay identical, since SubToAPI exposes the same messages streaming contract. See /docs/streaming for the exact event reference.

Handling tool-use deltas

If your request includes tools, content_block_delta events can carry input_json_delta chunks instead of plain text — partial JSON for a tool call being built up incrementally. A production-grade parser should branch on delta.type:

if (event.type === "content_block_delta") {
  if (event.delta?.type === "text_delta") {
    yield { kind: "text", value: event.delta.text ?? "" };
  } else if (event.delta?.type === "input_json_delta") {
    yield { kind: "tool_input", value: (event.delta as any).partial_json };
  }
}

Accumulate the partial_json strings per block index and JSON.parse once content_block_stop fires for that block. Full tool-calling details are in /docs/tools.

Typing the stream properly

Loose any typing is the most common mistake in TypeScript streaming code. Define discriminated unions for each event so your IDE and compiler catch mismatches:

type ContentBlockDelta =
  | { type: "text_delta"; text: string }
  | { type: "input_json_delta"; partial_json: string };

type MessageStreamEvent =
  | { type: "message_start"; message: { id: string } }
  | { type: "content_block_start"; index: number }
  | { type: "content_block_delta"; index: number; delta: ContentBlockDelta }
  | { type: "content_block_stop"; index: number }
  | { type: "message_delta"; delta: { stop_reason: string } }
  | { type: "message_stop" };

Now switch (event.type) narrows correctly inside each branch, and adding a new event type later is a one-line change instead of a runtime surprise.

Error handling and reconnects

Two things break streaming code in production: network drops mid-stream and malformed partial JSON lines at buffer boundaries. Always wrap the reader.read() loop in try/catch, and never assume a data: line is complete JSON — if JSON.parse throws, it usually means the chunk boundary split a JSON object across two reads, which the buffer-and-split logic above already guards against by holding the last incomplete line.

For retries, track the last message_start ID and treat any abrupt disconnect before message_stop as a failed turn rather than attempting to resume — Claude's API does not currently support resuming a partial stream by ID.

Getting started quickly

If you'd rather not maintain your own key rotation, usage tracking, and billing split across team members, SubToAPI wraps this same streaming interface behind a single API key (sub_live_...) with per-seat usage visible in a dashboard. The request/response shape above works unchanged — see /docs/quickstart for setup and /pricing for plan details, or jump straight to /signup to get a key and test the stream yourself.

Questions

Does Claude's streaming API use standard SSE? Yes. Each event is sent as event: and data: lines over a text/event-stream connection, so any standard SSE or fetch-based parser works without special libraries.

Can I stream Claude responses in the browser, not just Node? Yes — the fetch and ReadableStream APIs used in the example above are supported in modern browsers. Just be aware of CORS: calling the API directly from client-side JavaScript requires a server-side proxy to keep your API key secret.

What's the difference between text_delta and input_json_delta events? text_delta carries plain response text for rendering. input_json_delta carries partial JSON fragments for a tool call's arguments, which you accumulate and parse only after the block closes.

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 →