← Blog

Claude API Middleware for Express: A Practical Setup

2026-10-18 · 5 min read · SubToAPI Team

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:

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:

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.

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 →