Claude API Integration with Express Middleware
If you're building a Node.js backend and want to add Claude API integration with Express middleware, the core pattern is straightforward: wrap your Claude calls in Express middleware functions that handle authentication, request validation, streaming responses, and error handling before your route handlers ever touch the response object. This keeps your route logic clean and lets you reuse the same Claude-calling code across multiple endpoints.
This article walks through a working middleware setup for Express, including how to structure the middleware chain, stream Server-Sent Events back to the client, handle tool use, and centralize error handling — plus where a service like SubToAPI can remove some of the boilerplate around API key management entirely.
Why use middleware for Claude API calls
Express middleware is the natural place to put anything that needs to run before a route handler executes, or that multiple routes share. For a Claude-backed API, that typically includes:
- Authentication — verifying the caller's own app API key before you spend tokens on their behalf
- Request shaping — normalizing incoming payloads into the message format Claude expects
- Rate limiting — protecting your own backend from being hammered
- Logging and usage tracking — recording token counts and latency per request
- Error normalization — turning provider errors into a consistent shape for your frontend
Without middleware, you end up duplicating this logic in every route that talks to Claude. With it, you write it once.
Basic middleware structure
Here's a minimal setup with an Express app and a middleware function that attaches a configured client to req:
const express = require('express');
const app = express();
app.use(express.json());
function claudeClient(req, res, next) {
req.claude = {
apiKey: process.env.SUBTOAPI_KEY,
baseUrl: 'https://api.subtoapi.app/v1',
};
next();
}
app.use(claudeClient);
Every downstream route handler can now read req.claude instead of re-reading environment variables or reconstructing config.
Request validation middleware
Before you call any Claude API, validate the shape of the incoming request so you fail fast with a clear error instead of a confusing 400 from the provider:
function validateChatRequest(req, res, next) {
const { messages } = req.body;
if (!Array.isArray(messages) || messages.length === 0) {
return res.status(400).json({ error: 'messages array is required' });
}
for (const m of messages) {
if (!m.role || !m.content) {
return res.status(400).json({ error: 'each message needs role and content' });
}
}
next();
}
Chain it onto your route:
app.post('/chat', validateChatRequest, async (req, res, next) => {
try {
const response = await fetch(`${req.claude.baseUrl}/messages`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${req.claude.apiKey}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
model: 'claude-sonnet-4',
max_tokens: 1024,
messages: req.body.messages,
}),
});
const data = await response.json();
res.json(data);
} catch (err) {
next(err);
}
});
Streaming responses through Express
If you want token-by-token output in the browser, you need middleware that sets the right headers and pipes the upstream stream through without buffering:
function sseHeaders(req, res, next) {
res.setHeader('Content-Type', 'text/event-stream');
res.setHeader('Cache-Control', 'no-cache');
res.setHeader('Connection', 'keep-alive');
res.flushHeaders();
next();
}
app.post('/chat/stream', validateChatRequest, sseHeaders, async (req, res) => {
const upstream = await fetch(`${req.claude.baseUrl}/messages`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${req.claude.apiKey}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
model: 'claude-sonnet-4',
max_tokens: 1024,
messages: req.body.messages,
stream: true,
}),
});
upstream.body.on('data', (chunk) => res.write(chunk));
upstream.body.on('end', () => res.end());
upstream.body.on('error', () => res.end());
});
Separating sseHeaders from the route handler means you can reuse it for any streaming endpoint, not just chat. See /docs/streaming for the full event format if you're using SubToAPI as the upstream.
Centralized error handling middleware
Express error-handling middleware (the four-argument kind) is the right place to normalize provider errors — rate limits, invalid requests, upstream timeouts — into a single response shape:
app.use((err, req, res, next) => {
console.error(err);
const status = err.status || 500;
res.status(status).json({
error: err.message || 'Internal error',
code: err.code || 'unknown_error',
});
});
Put this at the very end of your middleware chain, after all routes. Any next(err) call from an earlier handler routes here automatically.
Handling tool use in middleware
If your endpoints use Claude's tool calling, it's worth adding a middleware step that intercepts tool_use responses and dispatches to your local function implementations before returning a final answer to the client:
async function resolveToolCalls(req, res, next) {
if (res.locals.claudeResponse?.stop_reason === 'tool_use') {
const toolResults = await Promise.all(
res.locals.claudeResponse.content
.filter((c) => c.type === 'tool_use')
.map(runLocalTool)
);
res.locals.toolResults = toolResults;
}
next();
}
This keeps tool execution logic out of your route handlers entirely. Check /docs/tools for the request/response schema tool calls follow.
Where SubToAPI fits
The middleware patterns above work with any Claude-compatible endpoint. SubToAPI handles the part that's often the most annoying in production: managing application API keys (sub_live_...), streaming, usage metadata, and team seats in a dashboard, so your Express middleware only has to worry about your own app logic, not credential rotation or per-user quota tracking. Swapping the base URL and key in the claudeClient middleware above is usually the entire migration. Get a key at /signup and see request/response shapes in /docs/messages.
FAQ
Do I need a separate middleware for every Claude endpoint?
No. Compose small, focused middleware functions (auth, validation, headers) and chain them per route as needed. Most Express apps end up with 3-5 reusable middleware functions covering all Claude-backed routes.
How do I handle rate limit errors from within middleware?
Catch the error in your route handler or async wrapper, attach a status and code to it, and call next(err). Your centralized error-handling middleware then returns a consistent JSON shape regardless of which route triggered it.
Can I stream Claude responses through Express to a browser client?
Yes, using Server-Sent Events. Set the correct headers early in the middleware chain with res.flushHeaders(), then pipe the upstream response body directly into res.write() calls as chunks arrive.