dev.to8 de junio de 2026 AFECTA AL EXAMEN
ModeloAPI

Building a streaming Claude client in the browser — without the SDK

Why I skipped the official Anthropic SDK for browser work, and the ~150 lines of TypeScript that replaced it

---

title: Building a streaming Claude client in the browser — without the SDK

published: true

canonical_url: https://ferhatatagun.com/blog/browser-only-claude-streaming

description: Why I skipped the official Anthropic SDK for browser work, and the ~150 lines of TypeScript that replaced it

tags: claude, anthropic, typescript, webdev

cover_image:

---

I wanted to call Claude from a browser. The Anthropic SDK said no — sort of.

When I tried `import Anthropic from "@anthropic-ai/sdk"` in a Next.js app, the bundler crashed. The error pointed at `node:fs/promises`, deep inside the package — an agent-toolset module that reads files from disk and obviously cannot run in a browser. It isn't optional code; it's pulled in by the SDK's main client entry.

So either I waited for a browser-clean entry point (eventually, maybe), or I talked to the Messages API directly. The endpoint is HTTP. The streaming format is Server-Sent Events. I'd done this for OpenAI before — how hard could it be?

Turns out: about 150 lines of TypeScript for a usable client, and the result is cleaner than the SDK for the kind of tool I was building. Here's what that took and why I'd recommend it for anything browser-side that touches the Claude API.

**TL;DR**

  • The official SDK pulls in Node-only modules and breaks browser bundles.
  • Direct `fetch` works once you send `anthropic-dangerous-direct-browser-access: true`.
  • The streaming format is straightforward SSE — split events on `\n\n`, parse `data:` lines.
  • The only mild gotcha is `tool_use` blocks: their `input` arrives as `input_json_delta` chunks you accumulate and parse at `content_block_stop`.
  • Hand-rolled means tiny bundle, fewer abstractions, full visibility into what the protocol is doing.

The CORS unlock

Browsers won't let you `fetch()` `https://api.anthropic.com` by default. Anthropic ships a flag to allow it: send `anthropic-dangerous-direct-browser-access: true` and CORS opens up. The header's name is a warning — keys typed into a browser are visible to anyone with devtools open. For a bring-your-own-key developer tool that's fine; for a production app shipping a server-side key, it isn't.

With the header in place, a minimal request looks like this:

ts
await fetch("https://api.anthropic.com/v1/messages", {
  method: "POST",
  headers: {
    "content-type": "application/json",
    "x-api-key": apiKey,
    "anthropic-version": "2023-06-01",
    "anthropic-dangerous-direct-browser-access": "true",
  },
  body: JSON.stringify({
    model,
    max_tokens: 1024,
    messages: [{ role: "user", content: "Hello." }],
    stream: true,
  }),
});

`stream: true` gives back a Server-Sent Events stream. The response body is a `ReadableStream<Uint8Array>` — chunks of bytes you decode as text. Events are delimited by a blank line; each event is a couple of lines (`event: <type>` and `data: <json>`), and the meaningful payload lives in `data:`.

What the stream actually looks like

For a plain text response, th

Leer artículo completo en dev.to