← Blog

Claude API Middleware for Express.js: A Working Setup

2026-09-24 · 5 min read · SubToAPI Team

Claude API middleware for Express.js is a small layer of code that sits between your routes and Anthropic's (or a proxy's) HTTP API, handling the repetitive stuff: attaching auth headers, streaming responses back to the client, catching rate limits, and logging what went out and what came back. You don't need a framework for this — a single Express middleware function or router-level handler covers most use cases.

This article walks through a practical implementation you can drop into an existing Express app, plus the tradeoffs between building it yourself and using a hosted layer like SubToAPI to skip the boilerplate entirely.

Why you need middleware instead of calling the API inline

Calling fetch or an SDK method directly inside a route handler works fine for a prototype. It stops working once you have more than one route that talks to Claude, because you end up duplicating:

Middleware centralizes all of that in one place. When you need to switch providers, add a fallback, or change your logging format, you edit one file instead of every route.

A basic middleware structure

Here's a minimal middleware that attaches a configured client to req and handles common failure modes:

// middleware/claude.js
import Anthropic from "@anthropic-ai/sdk";

const client = new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY });

export function claudeMiddleware(req, res, next) {
  req.claude = client;
  next();
}

export async function withRetry(fn, retries = 3) {
  for (let attempt = 0; attempt < retries; attempt++) {
    try {
      return await fn();
    } catch (err) {
      const status = err?.status;
      if (status === 429 || status >= 500) {
        const delay = 2 ** attempt * 500;
        await new Promise((r) => setTimeout(r, delay));
        continue;
      }
      throw err;
    }
  }
  throw new Error("Max retries exceeded");
}

Wire it into your app:

import express from "express";
import { claudeMiddleware, withRetry } from "./middleware/claude.js";

const app = express();
app.use(express.json());
app.use(claudeMiddleware);

app.post("/api/chat", async (req, res) => {
  try {
    const response = await withRetry(() =>
      req.claude.messages.create({
        model: "claude-sonnet-4-5",
        max_tokens: 1024,
        messages: req.body.messages,
      })
    );
    res.json(response);
  } catch (err) {
    res.status(500).json({ error: err.message });
  }
});

This pattern — attach client to req, wrap calls in a retry helper — scales fine for a single service.

Streaming middleware

Streaming needs its own handling because you can't just res.json() a stream. The middleware should set the right headers and pipe chunks as they arrive:

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 = req.claude.messages.stream({
    model: "claude-sonnet-4-5",
    max_tokens: 1024,
    messages: req.body.messages,
  });

  stream.on("text", (text) => {
    res.write(`data: ${JSON.stringify({ text })}\n\n`);
  });

  stream.on("end", () => {
    res.write("data: [DONE]\n\n");
    res.end();
  });

  stream.on("error", (err) => {
    res.write(`data: ${JSON.stringify({ error: err.message })}\n\n`);
    res.end();
  });

  req.on("close", () => stream.abort());
});

Note the req.on("close") handler — if the client disconnects, abort the upstream stream so you're not paying for tokens nobody reads.

Logging middleware

A separate middleware layer for logging keeps observability out of your business logic:

export function logClaudeUsage(req, res, next) {
  const start = Date.now();
  const originalJson = res.json.bind(res);

  res.json = (body) => {
    const duration = Date.now() - start;
    console.log({
      path: req.path,
      duration_ms: duration,
      input_tokens: body?.usage?.input_tokens,
      output_tokens: body?.usage?.output_tokens,
      model: body?.model,
    });
    return originalJson(body);
  };

  next();
}

Chain it before your route handler and you get consistent usage logs without touching each endpoint's logic.

Handling auth for multiple apps or clients

If your Express app serves multiple frontends or tenants, each needing its own rate limits and usage tracking, per-tenant API keys become useful. Rather than reimplementing key issuance, expiry, and per-key usage dashboards yourself, a service like SubToAPI issues scoped sub_live_... keys tied to your Claude access, with usage metadata already tracked per key. Your Express middleware then just swaps the upstream URL and forwards the appropriate key:

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-sonnet-4-5",
    max_tokens: 1024,
    messages: req.body.messages,
  }),
});

This is useful specifically when you don't want to build key management, per-app rate limiting, and team seat billing into your Express app yourself. The quickstart and Messages docs cover the request shape, and streaming and tool use work the same way through the middleware pattern above, just pointed at a different base URL.

Error normalization

One thing worth adding regardless of which backend you call: a consistent error shape so your frontend doesn't need provider-specific error handling.

export function normalizeClaudeError(err) {
  return {
    error: true,
    status: err.status || 500,
    message: err.message || "Unknown error",
    type: err.type || "api_error",
  };
}

Use this in a catch-all error handler at the end of your middleware chain so every route returns the same error structure.

Putting it together

A production-ready setup typically chains: auth check → logging → the Claude/SubToAPI client attachment → retry wrapper → route handler → error normalizer. Each piece is independently testable and swappable. Start with the retry and logging middleware since those catch the most common production issues (rate limits and silent cost overruns), then add streaming support once you have a UI that needs token-by-token rendering.

FAQs

Do I need a library, or is a plain Express middleware function enough? A plain middleware function is enough for most apps. Reach for a library only if you need advanced routing across multiple providers or built-in circuit breakers — otherwise the patterns above cover auth, retries, streaming, and logging without added dependencies.

How do I handle rate limits in Express middleware without losing requests? Wrap calls in an exponential backoff retry helper (as shown above) and return a 429 to your own clients only after retries are exhausted. Queueing requests server-side works too, but adds complexity most apps don't need until traffic is high.

Can this middleware work with a proxy like SubToAPI instead of calling Anthropic directly? Yes — the middleware pattern doesn't change, you just point the client or fetch call at the proxy's base URL and pass its API key. See the quickstart for the exact endpoint and header format.

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 →