Appearance
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:
- Proxied to port 3001 — if 404, falls through
- Proxied to port 3002 — if 404, falls through
- 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: truetells the proxy not to automatically pipe the response. This givesonProxyResfull control.WeakMapcorrelates each request with itsnext()callback. SinceonProxyResdoesn't receivenext, the outer middleware stashes it before proxying.WeakMap(notMap) 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
| Approach | Pros | Cons |
|---|---|---|
| Fallthrough proxy | Transparent | Latency per 404 |
| Path-based routing | One proxy call | Routes upfront |
| Service mesh | Production-grade | Infra 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.