Claude API Middleware for Express: A Practical Setup
If you're adding Claude to an Express app, you don't want every route handler calling the Anthropic SDK directly. You want a middleware layer that handles authentication, request validation, streaming, error normalization, and logging in one place — so your actual route logic stays thin. This article walks through building that middleware, plus where it makes sense to swap it for a hosted API instead of maintaining it yourself.
The short answer: Claude API middleware for Express is a function (or small set of functions) sitting between the incoming HTTP request and the Anthropic client call. It validates the payload, injects the API key, manages streaming responses with res.write(), catches SDK errors and maps them to proper HTTP status codes, and optionally logs token usage. Below is a working implementation you can adapt.
Why you need a middleware layer at all
Calling anthropic.messages.create() inside every route works fine for a prototype. It breaks down once you have multiple endpoints that need Claude: a chat endpoint, a summarization endpoint, a tool-calling endpoint. Without middleware you end up copy-pasting:
- API key handling and header checks
- Error translation (Anthropic errors aren't Express-friendly out of the box)
- Streaming boilerplate (SSE headers, chunk flushing, abort handling)
- Request body validation (model name, max_tokens, message format)
Middleware centralizes this so each route only needs to describe what to send to Claude, not how.
Basic middleware structure
Here's a minimal setup using the official Anthropic SDK:
const Anthropic = require('@anthropic-ai/sdk');
const anthropic = new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY });
function claudeMiddleware(req, res, next) {
const { messages, model = 'claude-3-5-sonnet-20241022', max_tokens = 1024 } = req.body;
if (!Array.isArray(messages) || messages.length === 0) {
return res.status(400).json({ error: 'messages array is required' });
}
req.claude = { anthropic, model, max_tokens, messages };
next();
}
app.use('/api/chat', claudeMiddleware);
app.post('/api/chat', async (req, res) => {
try {
const response = await req.claude.anthropic.messages.create({
model: req.claude.model,
max_tokens: req.claude.max_tokens,
messages: req.claude.messages,
});
res.json(response);
} catch (err) {
next(err);
}
});
This separates validation (middleware) from execution (route handler), which is already an improvement over inline calls.
Handling streaming in middleware
Streaming is where most hand-rolled middleware gets messy. You need to set the right headers, flush chunks as they arrive, and handle client disconnects.
async function streamClaude(req, res) {
res.setHeader('Content-Type', 'text/event-stream');
res.setHeader('Cache-Control', 'no-cache');
res.setHeader('Connection', 'keep-alive');
try {
const stream = await req.claude.anthropic.messages.stream({
model: req.claude.model,
max_tokens: req.claude.max_tokens,
messages: req.claude.messages,
});
stream.on('text', (text) => {
res.write(`data: ${JSON.stringify({ text })}\n\n`);
});
stream.on('end', () => {
res.write('data: [DONE]\n\n');
res.end();
});
req.on('close', () => stream.abort?.());
} catch (err) {
res.write(`data: ${JSON.stringify({ error: err.message })}\n\n`);
res.end();
}
}
Worth noting: you need to handle req.on('close') to abort the stream if the client disconnects, otherwise you're paying for tokens nobody receives.
Error-handling middleware
Anthropic SDK errors have their own shape (status codes, error types). Mapping them to Express error responses should happen in one place, not scattered across routes:
function claudeErrorHandler(err, req, res, next) {
if (err.status === 429) {
return res.status(429).json({ error: 'rate_limited', retry_after: err.headers?.['retry-after'] });
}
if (err.status === 401) {
return res.status(401).json({ error: 'invalid_api_key' });
}
if (err.status === 400) {
return res.status(400).json({ error: 'bad_request', detail: err.message });
}
console.error('Claude error:', err);
res.status(500).json({ error: 'internal_error' });
}
app.use(claudeErrorHandler);
This keeps your business logic routes clean of try/catch error-mapping noise.
Rate limiting and usage tracking
A production middleware stack usually adds a rate limiter in front of the Claude call — per-user or per-API-key — and logs token usage after each response for billing or quota purposes:
function logUsage(req, res, next) {
const originalJson = res.json;
res.json = function (data) {
if (data?.usage) {
console.log(`user=${req.user?.id} input=${data.usage.input_tokens} output=${data.usage.output_tokens}`);
}
return originalJson.call(this, data);
};
next();
}
Stack this before your route and you get usage logging without touching the handler itself.
When to stop building this yourself
Hand-rolled middleware is reasonable for a single app with a few endpoints. It gets harder to justify once you have multiple services, multiple API keys per client, team seats, or need usage dashboards and spend controls — now you're maintaining auth, metering, and streaming infrastructure instead of your product.
This is the gap SubToAPI fills: it turns your Claude access into a clean HTTPS API with its own sub_live_... application keys, built-in streaming, tool use, and per-key usage metadata — so your Express middleware just calls one endpoint instead of managing SDK internals, retries, and error mapping yourself.
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: req.claude.messages,
}),
});
Your Express middleware still validates input and handles response shaping — but the API key management, usage tracking, and streaming plumbing live behind SubToAPI's endpoint instead of in your codebase. Plans start at €9/month for solo use, with team and scale tiers for multi-seat setups; see /pricing. The /docs/quickstart walks through swapping your middleware's upstream call in under ten minutes, and /docs/streaming covers the SSE format in detail.
Keeping middleware maintainable
A few practices that keep this layer from becoming unwieldy as it grows:
- Validate early, fail fast. Reject malformed requests before they hit the API call.
- Separate streaming and non-streaming paths. Don't try to force one function to handle both.
- Centralize error mapping. One error-handling middleware, not try/catch blocks everywhere.
- Log usage metadata. Even a simple console log of token counts helps debug cost spikes later.
- Keep the middleware stateless. Store API keys and model config in environment variables, not hardcoded.
Questions
Does Claude's SDK require Express-specific middleware? No. The Anthropic SDK is framework-agnostic. Middleware is a pattern you build on top of it to handle validation, streaming, and errors consistently across routes.
Can I use this middleware pattern with streaming and tool use together? Yes, but keep them as separate code paths. Tool-use responses often require multiple round trips (model calls a tool, you return results, model continues), so streaming tool loops need their own handler logic — see /docs/tools for request/response shapes.
Is it better to proxy through my own middleware or call Claude directly from the frontend? Always proxy through your backend. Calling Claude directly from a browser exposes your API key. Middleware also lets you enforce rate limits, log usage, and swap providers without touching client code.