Claude API Node.js Quickstart Tutorial
What You'll Build
This tutorial gets you from zero to a working Claude integration in Node.js: installing dependencies, authenticating, sending your first message, handling the response, and streaming output to the console. By the end you'll have a small script you can extend into a chatbot, CLI tool, or backend endpoint.
You need Node.js 18+ (for native fetch), an API key, and about 10 minutes. We'll use plain fetch calls so the code works whether you're calling Anthropic's API directly or a compatible proxy like SubToAPI — the request/response shape is the same either way.
Step 1: Project Setup
Create a new project and initialize it:
mkdir claude-node-quickstart
cd claude-node-quickstart
npm init -y
Set "type": "module" in package.json so you can use ES module import syntax, or just use require if you prefer CommonJS — both work fine for the examples below.
Create a .env file (don't commit it) to hold your key:
SUBTOAPI_KEY=sub_live_xxxxxxxxxxxxxxxx
Install dotenv to load it:
npm install dotenv
Step 2: Your First Request
Create index.js:
import "dotenv/config";
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 what a quickstart tutorial should cover." }
]
})
});
const data = await response.json();
console.log(data.content[0].text);
Run it:
node index.js
If everything is wired correctly, you'll see Claude's reply printed to your terminal. That's the whole loop: build a request body with a model name, a token limit, and a messages array, send it with your bearer token, and read the text back out of content[0].text.
No SDK install is required for this — raw fetch is enough for most Node.js projects, which keeps your dependency tree small and avoids version-mismatch issues between an SDK and the underlying API.
Step 3: Handling Multi-Turn Conversations
Claude's API is stateless — you resend the full conversation history with every request. In Node.js, the natural pattern is to keep an array of messages and push to it:
const history = [
{ role: "user", content: "What's a good name for a note-taking app?" }
];
async function ask(messages) {
const res = 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: 512,
messages
})
});
const data = await res.json();
return data.content[0].text;
}
const reply = await ask(history);
history.push({ role: "assistant", content: reply });
history.push({ role: "user", content: "Make it one word." });
const reply2 = await ask(history);
console.log(reply2);
This is the core pattern behind any chatbot or assistant built on Claude: accumulate turns, resend the array, append the new assistant reply before the next user message.
Step 4: Streaming Responses
For anything user-facing, you don't want to wait for the full response before showing text. Add "stream": true and read the response body as a stream of server-sent events:
const res = 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,
stream: true,
messages: [{ role: "user", content: "Write a short haiku about Node.js." }]
})
});
const reader = res.body.getReader();
const decoder = new TextDecoder();
while (true) {
const { done, value } = await reader.read();
if (done) break;
const chunk = decoder.decode(value);
for (const line of chunk.split("\n")) {
if (line.startsWith("data: ")) {
try {
const event = JSON.parse(line.slice(6));
if (event.delta?.text) process.stdout.write(event.delta.text);
} catch {
// ignore non-JSON keepalive lines
}
}
}
}
This prints tokens as they arrive, which is what gives chat interfaces that "typing" feel. Full details on event types and delta shapes are in the streaming docs.
Step 5: Error Handling and Retries
Production code should handle rate limits and transient failures. A minimal retry wrapper:
async function sendWithRetry(body, attempts = 3) {
for (let i = 0; i < attempts; i++) {
const res = await fetch("https://api.subtoapi.app/v1/messages", {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${process.env.SUBTOAPI_KEY}`
},
body: JSON.stringify(body)
});
if (res.ok) return res.json();
if (res.status === 429 && i < attempts - 1) {
await new Promise(r => setTimeout(r, 1000 * (i + 1)));
continue;
}
throw new Error(`Request failed: ${res.status} ${await res.text()}`);
}
}
Check response status codes explicitly rather than assuming success — this catches malformed requests early during development.
Why Route Through SubToAPI
If you're building this inside a team, SubToAPI turns a Claude subscription into a standard HTTPS API with application-scoped keys (sub_live_...), per-key usage metadata, and seat-based billing — useful once more than one developer or service needs access. The request format above is identical to what you'd use directly; you're just pointing at a different base URL and getting a dashboard for keys, usage, and streaming on top. Start with the quickstart guide or see full request/response details in the messages docs. Plans start at €9/month with a free trial — see pricing or sign up to get a key.
Next Steps
From here, the natural extensions are: adding tool/function calling so Claude can call your own functions (see the tools docs), persisting conversation history to a database instead of an in-memory array, and wrapping the fetch calls in an Express or Fastify route so your frontend talks to your backend instead of directly to the API.
Questions
Do I need the official Anthropic SDK to use Node.js with Claude? No. Node 18+ ships native fetch, which is enough to call the API directly with JSON requests. An SDK adds convenience methods but isn't required for a quickstart.
Why isn't my streaming response showing up incrementally? Make sure you're reading res.body as a stream with getReader() rather than calling res.json(), and that "stream": true is set in the request body — otherwise the API returns one complete JSON response.
Can I use this same code with a different Claude API provider? Yes, as long as the provider implements the same messages/streaming request shape — you typically only need to change the base URL and the bearer token used in the Authorization header.