Claude API Node Express Server Setup Guide
Setting up a Node Express server to work with the Claude API means building a backend route that receives a request from your frontend, forwards it (with your API key) to Claude, and returns the response — without ever exposing your key to the browser. This pattern is the same whether you're calling Anthropic's API directly or a drop-in HTTPS layer like SubToAPI.
The setup involves four pieces: an Express app with a POST route, an HTTP client to call the Claude endpoint, environment variables to store your key safely, and (optionally) streaming support for real-time token output. Below is a complete, working example you can copy into a new project.
Project Setup
Start with a fresh Node project and install Express plus dotenv for environment variables:
mkdir claude-express-server && cd claude-express-server
npm init -y
npm install express dotenv cors
Create a .env file at the project root:
ANTHROPIC_API_KEY=sk-ant-xxxxxxxx
PORT=3000
If you're using SubToAPI instead of calling Anthropic directly, store your SubToAPI key the same way:
SUBTOAPI_KEY=sub_live_xxxxxxxx
PORT=3000
Basic Express Server
Here's a minimal server that exposes a /chat endpoint and forwards the request body to Claude:
// server.js
import express from "express";
import cors from "cors";
import dotenv from "dotenv";
dotenv.config();
const app = express();
app.use(cors());
app.use(express.json());
app.post("/chat", async (req, res) => {
try {
const { messages, model = "claude-3-5-sonnet-20241022", max_tokens = 1024 } = req.body;
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, max_tokens, messages }),
});
if (!response.ok) {
const errorBody = await response.text();
return res.status(response.status).json({ error: errorBody });
}
const data = await response.json();
res.json(data);
} catch (err) {
console.error("Chat request failed:", err);
res.status(500).json({ error: "Internal server error" });
}
});
app.listen(process.env.PORT, () => {
console.log(`Server running on port ${process.env.PORT}`);
});
This gives you a working /chat endpoint that any frontend can POST to. The key never leaves your server — the browser only ever talks to your Express app. For the exact request/response shape, check the messages docs.
Testing the Endpoint
Before wiring up a frontend, confirm the route works with curl:
curl -X POST http://localhost:3000/chat \
-H "Content-Type: application/json" \
-d '{
"messages": [
{ "role": "user", "content": "Explain Express middleware in one sentence." }
]
}'
If this returns a valid JSON response with a content array, your setup is correct. If you get a 401, double-check your .env file is loaded and the key matches what's in your dashboard.
Adding Streaming Support
For chat-style UIs, streaming tokens as they're generated feels much faster than waiting for the full response. Express supports this with res.write() and a readable stream from the API:
app.post("/chat/stream", async (req, res) => {
const { messages, model = "claude-3-5-sonnet-20241022" } = req.body;
res.setHeader("Content-Type", "text/event-stream");
res.setHeader("Cache-Control", "no-cache");
res.setHeader("Connection", "keep-alive");
const upstream = 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,
max_tokens: 1024,
messages,
stream: true,
}),
});
for await (const chunk of upstream.body) {
res.write(chunk);
}
res.end();
});
This forwards the server-sent events straight through to your frontend, which can parse them with an EventSource or a streaming fetch reader. Full details on event formats are in the streaming docs.
Project Structure for Real Applications
A single-file server works for a prototype, but once you add conversation history, auth, or tool use, split things up:
routes/chat.js— route handlersservices/claude.js— the fetch wrapper and request buildermiddleware/auth.js— your own user authentication (separate from the Claude/SubToAPI key).env— secrets, never committed
Keep your Claude or SubToAPI key in services/claude.js only, and never pass it through to the frontend in any response payload.
Handling Tool Use and Multi-Turn Conversations
If your Express server needs to support tool calling (function calling), the request body changes to include a tools array and your route needs to handle the tool_use stop reason by executing the tool server-side and sending the result back in a follow-up request. This is where a Node backend is essential — tool execution almost always needs to happen server-side (database queries, API calls, file access). See the tools documentation for the request/response cycle.
For most teams, the fastest path to a working integration is to skip provisioning raw Anthropic credentials and use a service built for this exact pattern. SubToAPI turns your existing Claude access into a standard HTTPS API with sub_live_ keys, so your Express routes above work unchanged — you just point fetch at api.subtoapi.app and manage keys, usage, and seats from one dashboard instead of juggling raw credentials per environment.
Error Handling and Rate Limits
Wrap every Claude call in a try/catch and check response.ok before parsing JSON — a non-200 response won't always throw, it'll just return an error body. Log the status code and response text so you can distinguish between auth errors (401), rate limits (429), and malformed requests (400). For production traffic, add a retry with exponential backoff on 429s and 5xxs rather than failing the user's request outright.
Questions
Do I need a backend at all, or can I call Claude from the browser? You need a backend. Calling Claude directly from client-side JavaScript would expose your API key to anyone who opens dev tools. Express (or any server framework) exists specifically to keep that key server-side.
What's the minimum Express version needed for this setup? Express 4.x works fine with native fetch if you're on Node 18+. On older Node versions, install node-fetch as a dependency instead of relying on the global fetch.
How do I switch from a direct Anthropic integration to SubToAPI? Change the base URL to https://api.subtoapi.app/v1/messages, swap your Authorization header to use your sub_live_ key, and keep the rest of your Express code identical — the request and response shapes match. See the quickstart guide for the full migration path.