Skip to content

Proxy Middleware with Fallthrough

An Express proxy middleware that falls through to the next handler when the proxied server returns 404. Enables cascading proxy chains where multiple backend servers are tried in order.

When to Use

  • Composing multiple backend APIs behind a single entry point
  • Building a gateway that routes to whichever backend handles a given path
  • Development setups with multiple servers on different ports
  • Any proxy scenario where 404 should mean "try the next handler" instead of "respond with 404"

The Pattern

Stack multiple proxyWithFallthrough() middleware to try backend servers in order. Each proxy calls next() on 404 so Express continues to the next one:

javascript
import express from "express";
import { listen } from "./http.js";

const app = express();

// Try API 1, then API 2, then fall through
app.use(proxyWithFallthrough(3001));
app.use(proxyWithFallthrough(3002));
app.use((req, res) => {
  res.send("No backend handled this request");
});

const { url } = await listen(app, 3000);
console.log(`Gateway at ${url}`);

A request to GET /users:

  1. Proxied to port 3001 — if 404, falls through
  2. Proxied to port 3002 — if 404, falls through
  3. Hits the final handler

Multiple microservices

Route to named service ports and terminate with a standard 404 handler:

javascript
// Each service handles its own routes
app.use(proxyWithFallthrough(ports.userService));
app.use(proxyWithFallthrough(ports.orderService));
app.use(proxyWithFallthrough(ports.productService));
app.use(ExpressHandlers.notFound);

Development gateway

Run multiple dev servers and unify them behind one port:

javascript
import { run } from "./process.js";

// Start backend servers
run("node", "api-server.js");
run("node", "auth-server.js");

const gateway = express();
gateway.use(proxyWithFallthrough(4001));
gateway.use(proxyWithFallthrough(4002));
gateway.use(express.static("public"));

await listen(gateway, 3000);

Implementing proxyWithFallthrough()

The implementation uses http-proxy-middleware with selfHandleResponse and a WeakMap to stash Express's next() callback:

javascript
import { createProxyMiddleware }
  from "http-proxy-middleware";

function proxyWithFallthrough(port) {
  const reqNextMap = new WeakMap();
  
  const proxy = createProxyMiddleware({
    target: `http://localhost:${port}`,
    changeOrigin: true,
    selfHandleResponse: true,
  
    onProxyRes(proxyRes, req, res) {
      if (proxyRes.statusCode === 404) {
        const next = reqNextMap.get(req);
        return next();
      }
      proxyRes.pipe(res);
    },
  });
  
  return function handle(req, res, next) {
    reqNextMap.set(req, next);
    return proxy(req, res, next);
  };
}

Key details

  • selfHandleResponse: true tells the proxy not to automatically pipe the response. This gives onProxyRes full control.
  • WeakMap correlates each request with its next() callback. Since onProxyRes doesn't receive next, the outer middleware stashes it before proxying.
  • WeakMap (not Map) ensures entries are garbage collected when the request object is released — no memory leak.
  • On 404, calling next() lets Express continue to the next middleware. On any other status, proxyRes.pipe(res) streams the response back normally.

Trade-offs

ApproachProsCons
Fallthrough proxyTransparentLatency per 404
Path-based routingOne proxy callRoutes upfront
Service meshProduction-gradeInfra overhead

Best for development and simple gateways. Each 404 adds a round-trip to the chain, so cascading many proxies in production may add latency. For production, prefer path-based routing or a proper service mesh.