← Blog

Claude API Integration with Express Middleware

2026-09-29 · 4 min read · SubToAPI Team

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:

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.

Turn your Claude access into an HTTPS API

SubToAPI gives you application API keys, streaming, tool use and usage insights on top of your existing Claude access — set up in minutes.

Start free  Read the quickstart →