Streaming to the Browser

Your server streams from Claude and forwards each chunk to the browser, which appends it live.

In a web app, the browser must never call Claude directly (the API key would leak). Instead, your server streams from Claude and re-streams the text to the browser.

The pipeline

  1. The browser requests an answer from your endpoint.
  2. Your server calls Claude with streaming on.
  3. As chunks arrive, your server writes them to the browser response.
  4. The browser appends each chunk to the page.
Tokens streaming from Claude through your server to the browserClaude APIYour serverre-streamsBrowser UItokentoken
Each token flows Claude to your server to the browser, appearing on the page as it arrives.

Example

Example Β· javascript
// Express route: stream Claude's reply to the browser as plain text chunks
app.post("/api/chat", async (req, res) => {
  res.setHeader("Content-Type", "text/plain; charset=utf-8");

  const stream = client.messages.stream({
    model: "claude-opus-4-8",
    max_tokens: 2048,
    messages: [{ role: "user", content: req.body.message }],
  });

  stream.on("text", (delta) => res.write(delta)); // forward each chunk
  await stream.finalMessage();
  res.end();
});

// In the browser: read the response body stream and append to the page
// const reader = (await fetch('/api/chat', {...})).body.getReader();

When to use it

  • Forward Claude SSE chunks from your Express server to the browser using the Fetch API with ReadableStream.
  • Display a typing indicator in the chat UI that disappears only when the stream_stop event arrives.
  • Append each streamed token to a contenteditable div so the user sees text appear character by character.

More examples

Server: stream Claude to the client

Sets SSE headers and forwards each text delta as a data: line so the browser can read it with EventSource.

Example Β· javascript
// Express route β€” streams Claude's response as SSE
app.post("/api/chat", async (req, res) => {
  res.setHeader("Content-Type", "text/event-stream");
  res.setHeader("Cache-Control", "no-cache");

  const stream = await client.messages.stream({
    model: "claude-haiku-4-5",
    max_tokens: 512,
    messages: req.body.messages,
  });

  for await (const chunk of stream) {
    if (chunk.type === "content_block_delta") {
      res.write(`data: ${JSON.stringify({ text: chunk.delta.text })}\n\n`);
    }
  }
  res.write("data: [DONE]\n\n");
  res.end();
});

Browser: read the SSE stream

Uses the Fetch ReadableStream API to process incoming SSE lines and append each text chunk to the DOM.

Example Β· javascript
const response = await fetch("/api/chat", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ messages: [{ role: "user", content: userInput }] }),
});

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

while (true) {
  const { done, value } = await reader.read();
  if (done) break;
  const lines = decoder.decode(value).split("\n");
  for (const line of lines) {
    if (line.startsWith("data: ") && line !== "data: [DONE]") {
      const { text } = JSON.parse(line.slice(6));
      outputEl.textContent += text;
    }
  }
}

Show typing indicator while streaming

Shows a typing indicator before the loop starts and hides it when the stream is exhausted.

Example Β· javascript
const indicator = document.getElementById("typing-indicator");
indicator.hidden = false;

const response = await fetch("/api/chat", { /* ...options */ });
const reader = response.body.getReader();
const decoder = new TextDecoder();

while (true) {
  const { done, value } = await reader.read();
  if (done) break;
  // append text chunks...
}

indicator.hidden = true; // hide when stream ends

Discussion

  • Be the first to comment on this lesson.