Anthropic TypeScript SDK: Setup, Streaming, Tools, baseURL
To use the Anthropic TypeScript SDK, install @anthropic-ai/sdk, construct a client with new Anthropic({ apiKey }) on a server (never in the browser), and call client.messages.create() with a model, max_tokens and a messages array. The same client exposes messages.stream() for token-by-token output, typed error classes, retry and timeout options, and a baseURL option for routing through a gateway or relay.
Installing the Anthropic TypeScript SDK
The package is published as @anthropic-ai/sdk and works in plain JavaScript as well as TypeScript, since the type definitions ship inside the package. Pick whichever package manager your project already uses:
npm install @anthropic-ai/sdk
pnpm add @anthropic-ai/sdk
bun add @anthropic-ai/sdkThe SDK targets modern Node.js runtimes and also runs on edge platforms such as Cloudflare Workers, Vercel Edge Functions and Deno, because it relies on the standard fetch API rather than Node-only HTTP modules. If you are on an older Node LTS line and see a fetch is not defined error at startup, upgrade Node rather than polyfilling; the SDK's own documentation lists the supported runtime versions.
If you have worked with the Claude API over raw HTTP before, the SDK is a thin, typed wrapper over the same POST /v1/messages endpoint, so the request and response shapes you already know carry over. Our guide on how to use the Claude API covers the underlying request model if you want that background first.
Creating the client and handling the API key
The client reads ANTHROPIC_API_KEY from the environment when you pass nothing, so the most common construction is a zero-argument one. Passing the key explicitly is useful when you load secrets through a vault or a framework-specific config layer:
import Anthropic from "@anthropic-ai/sdk";
// Reads process.env.ANTHROPIC_API_KEY automatically
const client = new Anthropic();
// Or inject the key yourself
const explicit = new Anthropic({
apiKey: process.env.ANTHROPIC_API_KEY,
});Node and server frameworks. Keep the key in an environment variable or a secrets manager and construct the client in server-only code: an Express route, a Next.js Route Handler or Server Action, a Nuxt server route, a Fastify plugin. The browser talks to your server; your server talks to Anthropic.
Edge runtimes. Cloudflare Workers and similar platforms do not expose process.env. Read the key from the platform's bindings and pass it in as apiKey. The rest of the SDK usage is unchanged.
Never ship the key to the browser. A key bundled into client-side JavaScript is visible to anyone who opens DevTools, and it will be scraped and used against your account. The SDK refuses to run in a browser environment unless you set dangerouslyAllowBrowser: true, and that flag exists for narrow cases such as internal tools where every user already has access to the key. For a public product, the right pattern is a server endpoint that holds the key and forwards requests.
If the client constructs fine but requests fail with a 401, the key is wrong, revoked or set in the wrong shell; the checklist in Anthropic API key not working walks through the common causes.
Your first messages.create call
A request needs three things: a model id, a max_tokens ceiling, and a messages array that starts with a user turn. A system prompt is optional and sits at the top level, not inside messages:
const response = await client.messages.create({
model: "claude-sonnet-4-5",
max_tokens: 1024,
system: "You are a concise assistant for a developer documentation site.",
messages: [
{ role: "user", content: "Explain what a content block is in two sentences." },
],
});
console.log(response.id, response.stop_reason, response.usage);Replace claude-sonnet-4-5 with whichever model id your account or gateway exposes. Model ids change over time, so treat the string as configuration rather than a constant baked into your code.
The response is an Anthropic.Message. Its content field is an array of blocks, not a single string, which is the most common surprise for developers coming from other chat APIs.
Reading content blocks with type narrowing
response.content is typed as a discriminated union: each block has a type field such as "text", "tool_use" or "thinking", and only the text variant carries a .text property. TypeScript will refuse response.content[0].text until you narrow, which is exactly the behaviour you want:
for (const block of response.content) {
if (block.type === "text") {
process.stdout.write(block.text);
} else if (block.type === "tool_use") {
console.log("tool requested:", block.name, block.input);
}
}
// Collapse all text blocks into one string when that is all you need
const text = response.content
.filter((b) => b.type === "text")
.map((b) => b.text)
.join("");Also check response.stop_reason. "end_turn" means a normal finish, "max_tokens" means your ceiling cut the answer short, and "tool_use" means the model is asking you to run a tool before it continues.
Streaming with client.messages.stream()
For anything shown to a user in real time, or for long outputs where a non-streaming request risks hitting the HTTP timeout, use client.messages.stream(). It takes the same parameters as create() and returns a MessageStream that you can consume in two ways.
Event helpers. The stream is an event emitter. The text event hands you just the delta string, and finalMessage() resolves to the complete Anthropic.Message once the stream ends:
const stream = client.messages.stream({
model: "claude-sonnet-4-5",
max_tokens: 4096,
messages: [{ role: "user", content: "Write a short changelog entry." }],
});
stream.on("text", (delta) => {
process.stdout.write(delta);
});
const finalMessage = await stream.finalMessage();
console.log("\nstop_reason:", finalMessage.stop_reason);
console.log("output tokens:", finalMessage.usage.output_tokens);Raw events with for await. When you need every server-sent event, for example to render tool-input JSON as it arrives, iterate the stream directly and switch on event.type:
for await (const event of stream) {
if (event.type === "content_block_delta" && event.delta.type === "text_delta") {
process.stdout.write(event.delta.text);
}
}Do not wrap .on("text") in a hand-rolled new Promise() to wait for completion; finalMessage() already handles completion, abort and error states. To cancel a stream early, call stream.abort() or pass an AbortSignal via the request options. The event sequence and how to forward it to a browser over SSE are covered in detail in Claude API streaming.
Tool use basics
Tools let the model ask your code to run a function. You describe each tool with a name, a description and a JSON Schema input_schema; when the model wants to call one, the response contains a tool_use block and stop_reason is "tool_use". You execute the function, send back a tool_result block with the matching tool_use_id, and call the API again:
const tools = [
{
name: "get_order_status",
description: "Look up the fulfilment status of an order by its id.",
input_schema: {
type: "object",
properties: { order_id: { type: "string" } },
required: ["order_id"],
},
},
];
const messages = [{ role: "user", content: "Where is order A-1042?" }];
const first = await client.messages.create({
model: "claude-sonnet-4-5",
max_tokens: 1024,
tools,
messages,
});
if (first.stop_reason === "tool_use") {
const toolUse = first.content.find((b) => b.type === "tool_use");
const result = await lookupOrder(toolUse.input.order_id); // your function
messages.push({ role: "assistant", content: first.content });
messages.push({
role: "user",
content: [
{ type: "tool_result", tool_use_id: toolUse.id, content: JSON.stringify(result) },
],
});
const second = await client.messages.create({
model: "claude-sonnet-4-5",
max_tokens: 1024,
tools,
messages,
});
}Two habits keep this loop healthy. Always parse toolUse.input as structured data rather than string-matching on it, and if the model emits several tool_use blocks in one turn, return all of their tool_result blocks inside a single user message. For TypeScript projects the SDK also offers a beta tool-runner helper with Zod schemas that drives the loop for you; the manual loop above is the portable version that works without any beta flag.
Error classes you should catch
Every failure the SDK raises extends Anthropic.APIError, with a numeric status and the server's message. Subclasses exist per HTTP status so you can branch without inspecting strings:
Anthropic.AuthenticationError(401): wrong, revoked or missing key.Anthropic.PermissionDeniedError(403): the key is valid but not allowed to do this.Anthropic.NotFoundError(404): unknown model id or wrong path, which is common when abaseURLis misconfigured.Anthropic.BadRequestError(400): malformed parameters, for example amessagesarray that does not start with auserturn.Anthropic.RateLimitError(429): slow down; the SDK already retried.Anthropic.InternalServerError(5xx) andAnthropic.APIConnectionError(network failure or timeout).
Order your checks from most specific to least specific, and check APIConnectionError before the base APIError because in the TypeScript SDK it is a subclass of it:
try {
const res = await client.messages.create({ /* ... */ });
} catch (err) {
if (err instanceof Anthropic.RateLimitError) {
// back off, queue, or surface a "busy" state
} else if (err instanceof Anthropic.AuthenticationError) {
// fail fast: no retry will fix a bad key
} else if (err instanceof Anthropic.APIConnectionError) {
// network or timeout; safe to retry with a cap
} else if (err instanceof Anthropic.APIError) {
console.error(err.status, err.message);
} else {
throw err;
}
}maxRetries and timeout options
The client retries connection errors and 408, 409, 429 and 5xx responses with exponential backoff, two retries by default. The request timeout defaults to ten minutes and is expressed in milliseconds in the TypeScript SDK, which trips up people coming from the Python SDK where it is seconds. Both can be set on the client or overridden per request through the second argument to any method:
const client = new Anthropic({
maxRetries: 3,
timeout: 60_000, // 60 seconds, in milliseconds
});
// Per-request override: no retries, short timeout for a health check
const probe = await client.messages.create(
{
model: "claude-sonnet-4-5",
max_tokens: 16,
messages: [{ role: "user", content: "ping" }],
},
{ maxRetries: 0, timeout: 5_000 },
);Because timeouts are themselves retried, the worst-case wall clock is roughly timeout * (maxRetries + 1). For user-facing paths, prefer streaming plus a sane timeout over a very large non-streaming max_tokens, since a long non-streaming request is the usual cause of "the SDK hangs".
Pointing the SDK at a gateway with baseURL
The baseURL option replaces the default API origin. It is how you route the SDK through a corporate proxy, a self-hosted gateway for logging and quota control, or a third-party relay endpoint that speaks the Anthropic protocol. The SDK appends paths such as /v1/messages to whatever you pass, so supply the origin, not the full endpoint, unless your gateway documents otherwise:
const client = new Anthropic({
apiKey: process.env.GATEWAY_API_KEY,
baseURL: process.env.ANTHROPIC_BASE_URL, // e.g. your gateway or relay origin
});The SDK also reads ANTHROPIC_BASE_URL from the environment, so you can leave the code untouched and switch targets per deployment. When a gateway returns a 404 for a request that works against the official endpoint, the usual culprit is a duplicated /v1 segment; print the resolved URL from the error's message and compare. Note that some gateways only implement the OpenAI-style request shape; in that case you would use the OpenAI client instead of this SDK.
ROIBest AI is an API relay endpoint compatible with both the Anthropic and OpenAI protocols. If you route requests through it, the baseURL option above is where the endpoint is configured; details are at ai.roibest.com.
FAQ
Does the Anthropic TypeScript SDK work with plain JavaScript?
Yes. The package is the same for both; TypeScript users get type checking and autocompletion from the bundled declarations, while JavaScript users call the identical functions without annotations.
Why does response.content[0].text give a type error?
Because content is an array of different block types and only text blocks have a .text property. Narrow with block.type === "text" before reading it, or filter the array first.
Can I run the SDK in Cloudflare Workers or other edge runtimes?
Yes. The SDK uses the standard fetch API, so it runs on edge platforms. Read the API key from the platform's secret bindings and pass it as apiKey, since process.env is not available there.
How do I use the SDK with a relay or gateway endpoint?
Set baseURL in the constructor (or the ANTHROPIC_BASE_URL environment variable) to the gateway's origin and pass the key that gateway issued as apiKey. Everything else in your code stays the same.