Anthropic Claude API Node.js Setup Guide
Setting up the Anthropic Claude API in Node.js takes three things: an API key, the official SDK (or plain fetch), and a correctly formatted messages request. This guide walks through the full setup from a fresh project to a working chat request, plus streaming, error handling, and the common mistakes that trip people up.
If you just want the shortest path to a working call, skip to the "Minimal setup" section below. If you're deciding between the raw SDK and a hosted API wrapper, the last section covers that tradeoff too.
Prerequisites
- Node.js 18 or later (native
fetchsupport matters if you skip the SDK) - An Anthropic API key, or a key from a provider that proxies Claude access
- A package manager:
npm,pnpm, oryarn
Minimal setup with the official SDK
Install the SDK:
npm install @anthropic-ai/sdk
Create a .env file and load it with dotenv (or Node's built-in --env-file flag):
ANTHROPIC_API_KEY=sk-ant-...
Basic request:
import Anthropic from "@anthropic-ai/sdk";
const client = new Anthropic({
apiKey: process.env.ANTHROPIC_API_KEY,
});
const message = await client.messages.create({
model: "claude-sonnet-4-5",
max_tokens: 1024,
messages: [
{ role: "user", content: "Explain event loops in one paragraph." },
],
});
console.log(message.content[0].text);
Run it with node --env-file=.env index.js (Node 20+) or load .env manually with dotenv/config.
That's the entire setup for a synchronous request. Three things people usually get wrong:
- Forgetting
max_tokens. It's required, not optional, and there's no sane default — the API will reject the request. - Sending
contentas a plain string when using multi-turn history. For simple single-turn prompts a string works, but once you build conversation history,messagesneeds an array of{ role, content }objects in order. - Not setting a model that exists. Model names change; check the current list in your provider's docs before hardcoding one into production code.
Setup without the SDK (plain fetch)
If you don't want a dependency, or you're running on an edge runtime that doesn't support the SDK, plain fetch works fine:
const response = await fetch("https://api.anthropic.com/v1/messages", {
method: "POST",
headers: {
"content-type": "application/json",
"x-api-key": process.env.ANTHROPIC_API_KEY,
"anthropic-version": "2023-06-01",
},
body: JSON.stringify({
model: "claude-sonnet-4-5",
max_tokens: 1024,
messages: [{ role: "user", content: "Explain event loops in one paragraph." }],
}),
});
const data = await response.json();
console.log(data.content[0].text);
The anthropic-version header is required and easy to forget — without it you'll get a 400 error that doesn't clearly say why.
Streaming responses
For anything user-facing, streaming avoids a blank screen while Claude generates a long answer. With the SDK:
const stream = client.messages.stream({
model: "claude-sonnet-4-5",
max_tokens: 1024,
messages: [{ role: "user", content: "Write a haiku about Node.js." }],
});
for await (const event of stream) {
if (event.type === "content_block_delta") {
process.stdout.write(event.delta.text ?? "");
}
}
With plain fetch, set "stream": true in the body and parse the server-sent events manually — the SDK saves noticeable boilerplate here, which is the main reason to pull it in even for small projects.
Handling errors and rate limits
Anthropic's API returns standard HTTP status codes: 400 for malformed requests, 401 for bad auth, 429 for rate limits, 500/529 for server-side issues. A production-ready setup wraps calls with retry logic for 429 and 5xx:
async function callClaude(payload, retries = 3) {
try {
return await client.messages.create(payload);
} catch (err) {
if (retries > 0 && (err.status === 429 || err.status >= 500)) {
await new Promise((r) => setTimeout(r, 1000 * (4 - retries)));
return callClaude(payload, retries - 1);
}
throw err;
}
}
The Anthropic SDK already retries some errors internally, but explicit backoff is worth adding if you're calling the API in a loop (batch jobs, background workers) rather than a single user-triggered request.
Environment and project structure
For a typical Node.js backend, keep the API call logic isolated so you can swap providers or add caching later:
src/
claude/
client.js # SDK instantiation
chat.js # message-building helpers
routes/
chat.js # Express/Fastify route that calls claude/chat.js
.env
Never import @anthropic-ai/sdk directly into a frontend bundle — the API key would ship to the browser. If you need Claude access from a client-side app, put a thin server route in front of it, or use a provider that issues scoped, revocable keys meant for that purpose.
When a direct SDK setup isn't the right fit
The direct SDK setup above is the right call for a single backend service with one team and predictable usage. It gets more work when you need:
- Per-application API keys so a leaked key doesn't expose your entire account
- Usage breakdown by project or team member without building your own logging
- Team seats and shared billing instead of one shared
.envsecret - A stable HTTPS endpoint you don't have to re-auth against Anthropic directly
That's the gap SubToAPI fills — it sits on top of your existing Claude access and exposes it as a standard HTTPS API with its own sub_live_... keys per application, streaming, tool use, and usage metadata in one dashboard. The request shape is close enough to the pattern above that switching is mostly a base URL and key change:
const response = await fetch("https://api.subtoapi.app/v1/messages", {
method: "POST",
headers: {
"content-type": "application/json",
authorization: `Bearer ${process.env.SUBTOAPI_KEY}`,
},
body: JSON.stringify({
model: "claude-sonnet-4-5",
max_tokens: 1024,
messages: [{ role: "user", content: "Explain event loops in one paragraph." }],
}),
});
Full request/response details are in the messages docs and streaming docs. If you're setting up for a team rather than a solo project, check pricing and the quickstart — plans start at €9 for solo use, with per-seat pricing for teams.
Questions
Do I need the official SDK, or can I just use fetch? Fetch works fine for basic requests. The SDK is worth it mainly for streaming, built-in retries, and typed responses — skip it if you're on an edge runtime with size constraints.
What Node.js version do I need? Node 18+ is the practical minimum; Node 20+ gives you native .env file loading without extra dependencies.
Why does my request fail with a 400 error even though the JSON looks right? Check for a missing anthropic-version header (if using plain fetch), a missing max_tokens field, or a content field that isn't an array of role/content objects for multi-turn conversations.