Claude API Node.js Integration Example (With Code)
Claude API Node.js Integration Example (With Code)
If you're searching for a Claude API Node.js integration example, you're probably trying to add Claude to an existing backend — a Next.js app, an Express server, a worker queue — and you want working code, not just a conceptual overview. This article walks through exactly that: authenticating, sending a message, streaming a response, handling tool calls, and dealing with errors, all in plain Node.js.
The short version: Claude's API is a standard HTTPS JSON API, so integrating it into Node.js means making a POST request with fetch or axios, passing your API key in a header, and parsing the response (or reading a stream). There's no required SDK — you can do all of this with the fetch that ships in modern Node.js (v18+) and about 20 lines of code.
Prerequisites
Before writing any code, you need:
- Node.js 18 or newer (for native
fetchsupport; otherwise installnode-fetch) - An API key — either directly from a Claude provider, or a
sub_live_...key if you're using a gateway like SubToAPI that sits on top of your existing Claude access - A basic understanding of async/await, since every call is asynchronous
If you don't already have programmatic Claude access and don't want to deal with billing setup, request limits, or key rotation yourself, SubToAPI turns an existing Claude subscription into a standard API with sub_live_... keys, usage metadata, and team seats — useful if you're prototyping fast or building for multiple clients. The rest of this guide applies regardless of which key you're using.
A minimal integration
Here's the simplest possible Node.js integration — one function that sends a prompt and returns the text response:
async function askClaude(prompt) {
const response = await fetch('https://api.subtoapi.app/v1/messages', {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.SUBTOAPI_KEY}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
model: 'claude-sonnet-4-5',
max_tokens: 1024,
messages: [
{ role: 'user', content: prompt }
]
})
});
if (!response.ok) {
throw new Error(`Request failed: ${response.status}`);
}
const data = await response.json();
return data.content[0].text;
}
askClaude('Explain event loops in Node.js in two sentences.')
.then(console.log)
.catch(console.error);
That's the whole pattern: build a messages array, POST it, read content[0].text from the response. Full request and response fields are documented at /docs/messages.
Handling multi-turn conversations
Claude is stateless — it has no memory of previous calls. Your Node.js app is responsible for keeping track of conversation history and sending it back on every request:
let history = [];
async function chat(userMessage) {
history.push({ role: 'user', content: userMessage });
const res = await fetch('https://api.subtoapi.app/v1/messages', {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.SUBTOAPI_KEY}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
model: 'claude-sonnet-4-5',
max_tokens: 1024,
messages: history
})
});
const data = await res.json();
const reply = data.content[0].text;
history.push({ role: 'assistant', content: reply });
return reply;
}
For production apps, store history per user/session in a database rather than a module-level array, and trim it when it approaches the model's context window.
Streaming responses in Node.js
For chat UIs or CLIs, streaming tokens as they're generated is far better UX than waiting for the full response. Node.js handles this naturally with async iterables over the response body:
async function streamChat(prompt) {
const response = await fetch('https://api.subtoapi.app/v1/messages', {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.SUBTOAPI_KEY}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
model: 'claude-sonnet-4-5',
max_tokens: 1024,
stream: true,
messages: [{ role: 'user', content: prompt }]
})
});
for await (const chunk of response.body) {
const text = Buffer.from(chunk).toString('utf8');
for (const line of text.split('\n')) {
if (line.startsWith('data: ')) {
const payload = line.slice(6);
if (payload === '[DONE]') return;
const event = JSON.parse(payload);
if (event.delta?.text) process.stdout.write(event.delta.text);
}
}
}
}
This is a simplified SSE parser — in a real app you'd want a proper event-stream parser that handles partial chunks across network packets. See /docs/streaming for the exact event shapes and a more robust parsing example.
Tool use (function calling) from Node.js
If your integration needs Claude to call functions in your codebase — querying a database, hitting an internal API, running a calculation — you define tools as JSON schemas and handle the tool_use response yourself:
const tools = [{
name: 'get_order_status',
description: 'Look up the status of a customer order by ID',
input_schema: {
type: 'object',
properties: { order_id: { type: 'string' } },
required: ['order_id']
}
}];
async function runWithTools(prompt) {
const res = await fetch('https://api.subtoapi.app/v1/messages', {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.SUBTOAPI_KEY}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
model: 'claude-sonnet-4-5',
max_tokens: 1024,
tools,
messages: [{ role: 'user', content: prompt }]
})
});
const data = await res.json();
const toolCall = data.content.find(block => block.type === 'tool_use');
if (toolCall) {
const result = await lookupOrder(toolCall.input.order_id); // your own function
// send result back in a follow-up message with role "tool_result"
}
return data;
}
Tool call handling always follows this loop: Claude requests a tool → your Node.js code executes it → you send the result back as a new message. Details on the schema and multi-step tool loops are in /docs/tools.
Error handling and rate limits
Production integrations need to handle three failure modes explicitly: HTTP errors (4xx/5xx), malformed JSON in streaming chunks, and timeouts on long-running requests. A simple retry wrapper with exponential backoff covers most transient failures:
async function withRetry(fn, retries = 3) {
for (let i = 0; i < retries; i++) {
try {
return await fn();
} catch (err) {
if (i === retries - 1) throw err;
await new Promise(r => setTimeout(r, 2 ** i * 500));
}
}
}
If you're routing traffic through SubToAPI, each key's usage is visible in the dashboard, which makes it easier to spot a misbehaving retry loop before it burns through quota.
Getting started
If you want to skip provisioning and billing setup and just start calling an API from Node.js today, sign up at /signup, grab a sub_live_... key, and follow /docs/quickstart — it covers the same request shape used in the examples above. Pricing for Solo, Team, and Scale plans is on /pricing.
questions
Do I need an official SDK to use Claude from Node.js? No. The API is plain HTTPS/JSON, so fetch or axios is sufficient. An SDK just wraps the same requests shown above with convenience methods and types.
How do I keep conversation context across multiple Node.js requests? Claude doesn't store conversation state server-side. Your app must resend the full message history (or a summarized version of it) with every request.
What's the simplest way to stream Claude responses to a frontend? Parse the server-sent events in Node.js as shown in the streaming example, then forward each text delta over a WebSocket or SSE connection to your frontend as it arrives.