Claude API Integration with Express.js: A Setup Guide
Claude API Integration with Express.js
Integrating Claude into an Express.js app means building a backend route that accepts a request from your frontend, forwards it to Claude's Messages API, and returns the response (or streams it) back to the client. Express doesn't need any special adapter for this — it's a standard HTTPS call from your Node.js server using fetch or axios, wrapped in a route handler.
The main decisions you'll make are: which SDK or HTTP client to use, whether you need streaming, how you'll handle authentication keys for multiple users or projects, and how you'll manage errors like rate limits or timeouts. This guide walks through a working setup you can drop into an existing Express app today.
Project setup
Start with a standard Express project and the official Anthropic SDK, or plain fetch if you'd rather avoid a dependency.
npm install express dotenv
npm install @anthropic-ai/sdk
Create a .env file with your API key and load it with dotenv at the top of your entry file. Never hardcode keys in route files — this is the most common mistake in Express + Claude integrations, and it's how keys end up committed to git history.
A basic Claude route
Here's a minimal Express route that sends a user message to Claude and returns the reply as JSON:
require('dotenv').config();
const express = require('express');
const Anthropic = require('@anthropic-ai/sdk');
const app = express();
app.use(express.json());
const anthropic = new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY });
app.post('/api/chat', async (req, res) => {
try {
const { message } = req.body;
const response = await anthropic.messages.create({
model: 'claude-3-5-sonnet-20241022',
max_tokens: 1024,
messages: [{ role: 'user', content: message }],
});
res.json({ reply: response.content[0].text });
} catch (err) {
console.error(err);
res.status(500).json({ error: 'Claude request failed' });
}
});
app.listen(3000, () => console.log('Server running on port 3000'));
This is the core pattern for nearly every Claude + Express integration: parse the request body, call the model, shape the response, handle failures. Everything else — conversation history, streaming, tool use — builds on top of this.
Handling conversation history
Claude's API is stateless: it doesn't remember previous turns unless you send them. In an Express app, you typically store conversation history per session (in memory for prototypes, in a database like Postgres or Redis for production) and pass the full message array on each request:
app.post('/api/chat', async (req, res) => {
const { sessionId, message } = req.body;
const history = conversations.get(sessionId) || [];
history.push({ role: 'user', content: message });
const response = await anthropic.messages.create({
model: 'claude-3-5-sonnet-20241022',
max_tokens: 1024,
messages: history,
});
history.push({ role: 'assistant', content: response.content[0].text });
conversations.set(sessionId, history);
res.json({ reply: response.content[0].text });
});
Watch your token usage as history grows — long conversations cost more per request since the full context is resent every time. Trimming or summarizing older turns is a common production fix.
Streaming responses in Express
For chat UIs, streaming tokens as they're generated feels much faster than waiting for the full response. Express supports this with server-sent events:
app.post('/api/chat/stream', async (req, res) => {
res.setHeader('Content-Type', 'text/event-stream');
res.setHeader('Cache-Control', 'no-cache');
res.setHeader('Connection', 'keep-alive');
const stream = anthropic.messages.stream({
model: 'claude-3-5-sonnet-20241022',
max_tokens: 1024,
messages: [{ role: 'user', content: req.body.message }],
});
stream.on('text', (text) => {
res.write(`data: ${JSON.stringify({ text })}\n\n`);
});
stream.on('end', () => res.end());
stream.on('error', (err) => {
console.error(err);
res.end();
});
});
On the frontend, you'd consume this with EventSource or a fetch reader loop, appending each chunk to the UI as it arrives.
Error handling and rate limits
Claude API calls can fail for a few predictable reasons: rate limiting, invalid request format, context length exceeded, or transient network issues. Wrap calls in try/catch and check status codes so failures don't crash your Express process or return a bare 500 with no useful message:
try {
const response = await anthropic.messages.create({ /* ... */ });
res.json({ reply: response.content[0].text });
} catch (err) {
if (err.status === 429) {
return res.status(429).json({ error: 'Rate limited, retry shortly' });
}
if (err.status === 400) {
return res.status(400).json({ error: 'Invalid request to Claude API' });
}
res.status(502).json({ error: 'Upstream error' });
}
For production apps, add retry logic with exponential backoff for 429s and 5xx errors, and set a reasonable timeout so a slow Claude response doesn't hold an Express request open indefinitely.
Managing keys across environments and teams
Once your Express app is live and multiple developers or environments (staging, production, client demos) need Claude access, sharing a single raw API key gets messy fast — you lose visibility into who's using how much, and revoking access for one project means rotating the key for everyone.
This is where SubToAPI fits into an Express workflow: it turns your existing Claude access into scoped sub_live_... application keys, each with its own usage metadata, so you can issue a separate key per Express app or environment without changing your integration code — the request shape stays the same as calling Claude directly:
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-3-5-sonnet-20241022',
max_tokens: 1024,
messages: [{ role: 'user', content: req.body.message }],
}),
});
Streaming and tool use work the same way as with the direct Anthropic API — see /docs/streaming and /docs/tools for details, or /docs/quickstart to get set up. Plans start at €9/month on the Solo tier, with team seats at €19 and €49 for larger setups — check /pricing for the full breakdown.
Putting it together
A production-ready Express + Claude integration typically includes: a dedicated route module (not everything in index.js), environment-based key management, request validation with something like Zod or Joi, streaming for chat interfaces, retry logic for transient failures, and logging so you can debug bad Claude responses after the fact. Start with the basic route above, add streaming once your UI needs it, and layer in history management and error handling as your app grows.
questions
Do I need the official Anthropic SDK to use Claude with Express? No. The SDK simplifies request formatting and streaming, but Claude's API is plain HTTPS with JSON, so fetch or axios inside an Express route works fine too.
How do I stream Claude responses to a browser client from Express? Set SSE headers on the response, listen for streamed text chunks from the Claude API call, and res.write() each chunk as data: events, then res.end() when the stream completes.
Can I use one Claude API key across multiple Express environments? You can, but it removes per-environment usage visibility. Scoped keys — either issued manually per environment or through a service like SubToAPI — make it easier to track and revoke access independently.