The Anthropic TypeScript SDK Mental Model
The @anthropic-ai/sdk package is the official TypeScript and JavaScript client for the Claude API.
Search across all documentation pages
The @anthropic-ai/sdk package is the official TypeScript and JavaScript client for the Claude API.
Most developers meet it as npm install @anthropic-ai/sdk followed by a single messages.create call, and that call works fine as a first impression.
But the SDK's real shape only becomes clear once you understand the handful of design decisions underneath it: it is built on fetch rather than a Node-specific HTTP stack, it models a streaming response as an async iterable rather than a stream of callbacks, and it leans on TypeScript's discriminated unions so the compiler, not your memory, tracks what shape of data you are holding at any point.
This page builds that mental model before you write any production code against the SDK.
@anthropic-ai/sdk is a thin, fetch-based client that turns HTTP calls to the Claude API into typed objects and async iterables.fetch, the same client code runs in Node.js, Vercel Edge Functions, Cloudflare Workers, and Deno Deploy without a runtime-specific build.At its core, the SDK is a wrapper around HTTP requests to the Claude API.
You construct one Anthropic client, usually once per process or per request, and every subsequent call is a method on that client.
import Anthropic from "@anthropic-ai/sdk";
const client = new Anthropic({
apiKey: process.env.ANTHROPIC_API_KEY,
});The client object is deliberately unremarkable.
It holds your API key, your base URL, and your default request options (timeouts, retries, headers), and it exposes resource namespaces such as client.messages that group related methods.
Calling client.messages.create(...) sends a single HTTP request and resolves with a plain JavaScript object once the response arrives.
That object is not a special class instance with hidden behavior. It is JSON, typed.
A useful analogy is that the SDK is closer to a well-typed fetch wrapper than to a stateful framework.
It does not open a persistent connection, it does not remember previous turns of a conversation for you, and it does not decide when to call a tool.
Every one of those responsibilities stays in your application code. The SDK's job is narrower: turn a request object into a validated HTTP call, and turn the response back into typed TypeScript data.
Most HTTP clients in the Node ecosystem are built on Node's own http and https modules, which only exist in a Node.js process.
The Anthropic TypeScript SDK instead builds its request layer on the standard fetch API, using a Node-compatible fetch implementation when running under Node and the runtime's native fetch everywhere else.
That single choice is what lets the same import, the same client construction, and the same messages.create call run unmodified in a Vercel Edge Function, a Cloudflare Worker, or a Deno Deploy isolate, none of which expose Node's http module at all.
Practically, this means you do not choose a "browser build" versus a "server build" of the SDK.
You choose where to run your code, and the SDK adapts because its lowest layer only assumes the presence of fetch, ReadableStream, and AbortController, all of which are standard across modern JavaScript runtimes.
A non-streaming call waits for the full response body before resolving.
A streaming call is different: the server sends the response as a sequence of server-sent events, one per token or per structural change, and the SDK's job is to turn that raw event stream into something you can consume incrementally.
Many HTTP client libraries expose streaming as an EventEmitter, where you register on("data", ...) and on("end", ...) callbacks and manage state across calls yourself.
The Anthropic SDK instead returns a stream object that is itself an async iterable, so you consume it with a standard for await loop.
const stream = client.messages.stream({
model: "claude-sonnet-5",
max_tokens: 1024,
messages: [{ role: "user", content: "Explain async iterators." }],
});
for await (const event of stream) {
if (event.type === "content_block_delta") {
process.stdout.write(event.delta.text ?? "");
}
}The mental shift here matters more than the syntax.
An event-listener model forces you to build your own accumulator and your own notion of "done," usually with a mutable variable that lives outside the callback.
An async iterable model lets the control flow of your own function express that logic: the loop body runs once per event, and the loop simply ends when the stream ends, with no separate "on end" handler to keep in sync.
The stream object also exposes convenience methods, such as waiting for the final assembled message, so you are not required to manually concatenate deltas when you only care about the finished result.
A Claude response is not one fixed shape.
A single message can contain a text block, a tool use block, and (when extended thinking is enabled) a thinking block, in any combination and order.
The SDK models this with a discriminated union: every content block type shares a type field, and that field's literal value ("text", "tool_use", "thinking") is what TypeScript uses to narrow the type.
for (const block of message.content) {
switch (block.type) {
case "text":
console.log(block.text);
break;
case "tool_use":
console.log(block.name, block.input);
break;
}
}Once you branch on block.type, the compiler already knows which other fields exist on block inside that branch, text inside the "text" case, name and input inside the "tool_use" case, without a manual type assertion.
This is why reading Claude responses in TypeScript tends to look like control flow on a tag, rather than optional-chaining through a loosely typed blob.
The same discriminated-union pattern extends to streaming events (content_block_start, content_block_delta, message_stop, and others), so the shape of the code you write to handle a stream mirrors the shape of the code you write to handle a finished message.
Network calls fail, time out, and occasionally get rate limited, and the SDK treats all three as first-class configuration rather than problems you solve with your own retry loop.
You can set maxRetries and timeout globally on the client or per-call, and the SDK applies exponential backoff for retryable failures (server errors, connection errors, rate limits) automatically.
For cancellation, the SDK accepts a standard AbortController signal on any call, which lets you tie a Claude request's lifetime to something external, a user closing a chat panel, an HTTP request handler being cancelled, or a timeout you control yourself.
const controller = new AbortController();
setTimeout(() => controller.abort(), 5000);
await client.messages.create(
{ model: "claude-sonnet-5", max_tokens: 256, messages: [...] },
{ signal: controller.signal },
);Because AbortController is a web standard rather than a Node API, this cancellation mechanism works identically in a browser, an edge function, or a Node server, which is the same portability story as the fetch-based transport underneath it.
When a request fails, the SDK throws a specific error class rather than a generic HTTP error with a numeric status attached.
BadRequestError, RateLimitError, AuthenticationError, and APIConnectionError are each distinct classes, so a catch block can branch on instanceof rather than inspecting a status code and hoping the shape of the error body matches what you expect.
try {
await client.messages.create({ ... });
} catch (error) {
if (error instanceof Anthropic.RateLimitError) {
// back off and retry later
} else if (error instanceof Anthropic.APIConnectionError) {
// network-level failure, not an API rejection
}
}This matters most in production code that has to behave differently for different failure classes: a rate limit usually means "wait and retry," an authentication error usually means "stop and alert," and a connection error usually means "the network, not Claude, is the problem."
A common production pattern is wiring client.messages.stream() into a Next.js route handler so the handler forwards Claude's token stream straight through to the browser as a token-by-token chat response.
The SDK makes that pattern natural because its stream is already an async iterable and the underlying transport is already fetch-compatible with edge runtimes, but the SDK itself does not know anything about Next.js, your route structure, or how you present partial output to a user.
It also does not manage multi-turn conversation history (you resend prior messages yourself), and it does not run a tool-calling loop for you (you inspect tool_use blocks and decide whether and how to execute them).
Treating the SDK as "typed transport, not orchestration" keeps you from expecting agent-framework behavior from a client library.
| Approach | Strength | Weakness | Best Fit |
|---|---|---|---|
Non-streaming messages.create | Simplest code, one awaited response | User waits for the entire completion before seeing anything | Batch jobs, backend-to-backend calls with no live UI |
Streaming messages.stream with for await | Tokens appear as they generate, natural control flow | Slightly more code to assemble partial state if you need the full message too | Chat UIs, anywhere perceived latency matters |
| Streaming with the built-in final-message helper | Get incremental UI updates and the finished message from one call | Holds the full response in memory until the stream ends | Chat UIs that also need to persist the completed message |
Anthropic client and the same messages namespace expose both create (non-streaming) and stream (streaming); streaming is a method choice, not a separate installation.fetch-based, it runs in any environment that implements the fetch, ReadableStream, and AbortController standards, which includes most modern edge runtimes.type field the union narrows on is the same field the API actually returns, so the typing is not aspirational. Narrowing on block.type reflects a real structural difference in the response, not a convenience layer bolted on top.Because fetch is a web standard implemented by Node, browsers, and edge runtimes alike, the same SDK code runs unmodified on a Vercel Edge Function, a Cloudflare Worker, or a Deno Deploy isolate, none of which have Node's http module available.
import Anthropic from "@anthropic-ai/sdk";
const client = new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY });One client instance is enough for many requests; it just holds your configuration.
messages.create resolves once with the complete response.messages.stream returns an object you iterate over, receiving partial events as Claude generates them.client.messages namespace, so switching between them is a method choice, not a different client.It means you can consume it with a standard for await (const event of stream) loop, letting your own loop body react to each event as it arrives, instead of registering separate on("data") and on("end") event-listener callbacks and tracking completion state yourself.
It is a TypeScript pattern where every possible content block shares a common type field ("text", "tool_use", "thinking"), and checking that field's value lets the compiler narrow which other fields are available on that specific block, without a manual type cast.
No. The SDK accepts a maxRetries option (client-wide or per-call) and applies backoff automatically for retryable failures such as connection errors, certain server errors, and rate limits.
Pass an AbortController's signal as a request option and call controller.abort() when you want to cancel; this works the same way in Node, browsers, and edge runtimes because AbortController is a web standard, not a Node-specific API.
Catch the call and check instanceof against the SDK's typed error classes (RateLimitError, BadRequestError, AuthenticationError, APIConnectionError, and others) so your handling logic matches the actual failure kind instead of parsing a status code.
No. You are responsible for building the messages array on each call, including prior turns you want Claude to see. The SDK sends exactly what you give it and does not persist state between calls.
No. When Claude wants to use a tool, the response includes a tool_use content block; your code is responsible for reading it, executing the corresponding logic, and sending the result back in a subsequent call.
Choose non-streaming for backend-to-backend calls, batch processing, or anywhere no human is watching output arrive live, since it is simpler code with no partial-event handling. Choose streaming whenever perceived latency in a UI matters.
It reflects the real API. The discriminated union fields (like type on a content block) are the same fields the Claude API actually returns on the wire, so narrowing in TypeScript corresponds to a real structural distinction in the response, not a cosmetic layer.
Stack versions: Written against the Claude model lineup current as of ~June 2026 - Claude Fable 5, Claude Opus 4.8, Claude Sonnet 5 (the default), and Claude Haiku 4.5 - and the official
@anthropic-ai/sdkTypeScript SDK (latest release). Model names, SDK versions, and pricing move quickly - verify current specifics at platform.claude.com/docs before relying on them.
Reviewed by Chris St. John·Last updated Jul 16, 2026