← Blog

Claude API Integration with Express.js Backend

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

Integrating Claude into an Express.js backend

If you're building a Node.js service and want Claude to handle chat, summarization, extraction, or tool-calling logic, the integration pattern is straightforward: create an Express route that proxies requests to Claude's API, keep your API key server-side, and stream responses back to the client when the use case needs it. This article walks through that setup end to end, including streaming and error handling, which are the two parts developers most often get wrong on the first pass.

There are two practical ways to connect Claude to Express: calling the Anthropic API directly with your own key, or routing through a managed layer like SubToAPI that gives you application-scoped keys, usage metadata, and team billing on top of the same model access. Both approaches use the same request/response shape in your Express routes, so switching between them later is mostly a matter of changing the base URL and key.

Project setup

Install Express and the HTTP client you'll use. The native fetch available in Node 18+ is enough — no extra SDK required.

npm install express dotenv

Store your key in environment variables, never in code:

# .env
ANTHROPIC_API_KEY=sk-ant-xxxxx
PORT=3000

A basic Claude route in Express

Here's a minimal /chat endpoint that forwards a user message to Claude and returns the completion:

import express from "express";
import "dotenv/config";

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

app.post("/chat", async (req, res) => {
  const { message } = req.body;

  try {
    const response = await fetch("https://api.anthropic.com/v1/messages", {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
        "x-api-key": process.env.ANTHROPIC_API_KEY,
        "anthropic-version": "2023-06-01",
      },
      body: JSON.stringify({
        model: "claude-sonnet-4-5",
        max_tokens: 1024,
        messages: [{ role: "user", content: message }],
      }),
    });

    const data = await response.json();
    res.json({ reply: data.content?.[0]?.text });
  } catch (err) {
    console.error(err);
    res.status(500).json({ error: "Claude request failed" });
  }
});

app.listen(process.env.PORT, () => console.log("Server running"));

This is the full round trip: Express receives the request, forwards it to Claude, and returns a JSON reply. For most internal tools and prototypes, this pattern alone is enough.

Streaming responses back to the client

Chat UIs feel much faster when tokens stream in instead of waiting for the full response. Claude supports server-sent events via "stream": true, and Express can relay that stream directly to the browser:

app.post("/chat/stream", async (req, res) => {
  res.setHeader("Content-Type", "text/event-stream");
  res.setHeader("Cache-Control", "no-cache");
  res.setHeader("Connection", "keep-alive");

  const response = await fetch("https://api.anthropic.com/v1/messages", {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      "x-api-key": process.env.ANTHROPIC_API_KEY,
      "anthropic-version": "2023-06-01",
    },
    body: JSON.stringify({
      model: "claude-sonnet-4-5",
      max_tokens: 1024,
      stream: true,
      messages: req.body.messages,
    }),
  });

  for await (const chunk of response.body) {
    res.write(chunk);
  }
  res.end();
});

On the frontend, read this with EventSource or a fetch stream reader. The key point for Express: don't buffer the whole response before sending — pipe chunks as they arrive, and set the right headers so proxies (nginx, load balancers) don't try to buffer either.

Error handling and retries

Claude's API returns standard HTTP error codes — 429 for rate limits, 529 for overload, 400 for malformed requests. Your Express middleware should distinguish retryable errors from permanent ones:

function isRetryable(status) {
  return status === 429 || status === 529 || status >= 500;
}

async function callClaudeWithRetry(body, attempts = 3) {
  for (let i = 0; i < attempts; i++) {
    const res = await fetch("https://api.anthropic.com/v1/messages", {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
        "x-api-key": process.env.ANTHROPIC_API_KEY,
        "anthropic-version": "2023-06-01",
      },
      body: JSON.stringify(body),
    });
    if (res.ok) return res.json();
    if (!isRetryable(res.status) || i === attempts - 1) {
      throw new Error(`Claude error ${res.status}`);
    }
    await new Promise((r) => setTimeout(r, 500 * (i + 1)));
  }
}

Wrap your route handlers so a Claude failure returns a clean error to your client instead of an unhandled promise rejection crashing the process.

Using SubToAPI instead of raw Anthropic keys

Direct integration works fine for a single project, but it gets messy once you have multiple apps, environments, or team members needing access — you end up managing one shared key with no per-app visibility into usage or cost. SubToAPI sits between your Express backend and Claude: it issues application-scoped keys (sub_live_...), tracks usage per key, and supports team seats so you're not sharing one secret across every service.

The Express code barely changes — you call https://api.subtoapi.app/v1/messages with a sub_live_ bearer token instead of Anthropic's endpoint directly:

const response = await fetch("https://api.subtoapi.app/v1/messages", {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    "Authorization": `Bearer ${process.env.SUBTOAPI_KEY}`,
  },
  body: JSON.stringify({
    model: "claude-sonnet-4-5",
    max_tokens: 1024,
    messages: req.body.messages,
  }),
});

Streaming and tool use work the same way as the raw Anthropic API — see /docs/streaming and /docs/tools for request formats. If you're setting this up for the first time, /docs/quickstart covers key creation and your first request, and /docs/messages documents the full message schema. Plans start at Solo for solo projects and scale to Team and Scale tiers with per-seat pricing — check /pricing or start a free trial at /signup.

Production checklist

Before shipping a Claude-backed Express route to production:

questions

Do I need the Anthropic SDK, or is fetch enough? fetch is enough for most Express integrations. The official SDK adds convenience methods and typed responses, but the raw HTTP API works identically and avoids an extra dependency if you're already comfortable building the request body yourself.

How do I keep streaming responses working behind a reverse proxy? Disable response buffering on your proxy (e.g., proxy_buffering off in nginx) and set Content-Type: text/event-stream plus Connection: keep-alive headers in your Express route, as shown above.

Can multiple Express services share one Claude integration safely? Yes, but give each service its own key so you can track usage and revoke access independently. SubToAPI's per-application sub_live_ keys are built for exactly this, letting several Express backends call Claude through one dashboard without sharing credentials.

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 →