http-proxy-middleware vs node-http-proxy vs fastify-http-proxy: Reverse Proxy in Node.js (2026)
TL;DR
http-proxy-middleware is the Express/Connect proxy middleware — used by Create React App and Vite for dev server proxying, supports path rewriting and WebSocket forwarding. node-http-proxy (http-proxy) is the foundational library underneath — low-level, handles raw HTTP/HTTPS/WS proxying, maximum control. @fastify/http-proxy is Fastify's built-in proxy plugin — uses undici for high-performance proxying with Fastify's hook system. In 2026: http-proxy-middleware for Express dev-server proxying, node-http-proxy for custom proxy servers, @fastify/http-proxy for Fastify API gateways.
Key Takeaways
- http-proxy-middleware: ~8M weekly downloads — Express middleware, powers CRA/Vite dev proxy
- node-http-proxy (http-proxy): ~5M weekly downloads — foundational, low-level proxy engine
- @fastify/http-proxy: ~200K weekly downloads — undici-based, Fastify hooks, high performance
- http-proxy-middleware wraps node-http-proxy — adds middleware API, path rewriting, logging
- @fastify/http-proxy uses undici (not http-proxy) — different foundation, faster for Fastify
- Common use cases: dev server API proxy, microservice gateway, BFF (backend for frontend)
Why Proxy in Node.js?
Use case 1: Development proxy (avoid CORS):
Frontend (localhost:3000) → Proxy → Backend API (localhost:8080)
React/Vite dev server proxies /api/* to backend
Use case 2: API gateway:
Client → Gateway (Node.js) → Service A (/api/packages)
→ Service B (/api/users)
→ Service C (/api/billing)
Use case 3: BFF (Backend for Frontend):
Mobile app → BFF → combines data from multiple services
Web app → BFF → aggregates + transforms responses
Use case 4: Load balancing / A-B testing:
Client → Proxy → 90% → Production service
→ 10% → Canary service
http-proxy-middleware
http-proxy-middleware — Express/Connect proxy:
Basic setup
import express from "express"
import { createProxyMiddleware } from "http-proxy-middleware"
const app = express()
// Proxy /api/* requests to backend:
app.use("/api", createProxyMiddleware({
target: "http://localhost:8080",
changeOrigin: true,
}))
// Request: GET http://localhost:3000/api/packages/react
// Proxied: GET http://localhost:8080/api/packages/react
app.listen(3000)
Path rewriting
app.use("/api", createProxyMiddleware({
target: "http://localhost:8080",
changeOrigin: true,
pathRewrite: {
"^/api/v2": "/v2", // /api/v2/packages → /v2/packages
"^/api": "", // /api/packages → /packages
},
}))
Multiple targets
// Route different paths to different services:
app.use("/api/packages", createProxyMiddleware({
target: "http://package-service:3001",
changeOrigin: true,
pathRewrite: { "^/api/packages": "/packages" },
}))
app.use("/api/users", createProxyMiddleware({
target: "http://user-service:3002",
changeOrigin: true,
pathRewrite: { "^/api/users": "/users" },
}))
app.use("/api/billing", createProxyMiddleware({
target: "http://billing-service:3003",
changeOrigin: true,
}))
WebSocket proxying
app.use("/ws", createProxyMiddleware({
target: "http://localhost:8080",
ws: true, // Enable WebSocket proxying
changeOrigin: true,
}))
Request/response modification
app.use("/api", createProxyMiddleware({
target: "http://localhost:8080",
changeOrigin: true,
on: {
// Modify outgoing request:
proxyReq(proxyReq, req, res) {
proxyReq.setHeader("X-Forwarded-For", req.ip)
proxyReq.setHeader("X-Request-ID", crypto.randomUUID())
// Add auth header:
proxyReq.setHeader("Authorization", `Bearer ${getServiceToken()}`)
},
// Modify incoming response:
proxyRes(proxyRes, req, res) {
proxyRes.headers["x-proxy"] = "pkgpulse-gateway"
},
// Handle proxy errors:
error(err, req, res) {
console.error("Proxy error:", err.message)
res.writeHead(502, { "Content-Type": "application/json" })
res.end(JSON.stringify({ error: "Bad gateway", upstream: err.message }))
},
},
}))
Vite dev server proxy (built-in)
// vite.config.ts — uses http-proxy-middleware under the hood:
import { defineConfig } from "vite"
export default defineConfig({
server: {
proxy: {
"/api": {
target: "http://localhost:8080",
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, ""),
},
"/ws": {
target: "ws://localhost:8080",
ws: true,
},
},
},
})
node-http-proxy
node-http-proxy — low-level proxy:
Basic proxy server
import httpProxy from "http-proxy"
import http from "node:http"
// Create a proxy instance:
const proxy = httpProxy.createProxyServer({
target: "http://localhost:8080",
changeOrigin: true,
ws: true,
})
// Handle proxy errors:
proxy.on("error", (err, req, res) => {
console.error("Proxy error:", err.message)
if (res.writeHead) {
res.writeHead(502, { "Content-Type": "application/json" })
res.end(JSON.stringify({ error: "Bad gateway" }))
}
})
// Create HTTP server:
const server = http.createServer((req, res) => {
proxy.web(req, res)
})
// Handle WebSocket upgrade:
server.on("upgrade", (req, socket, head) => {
proxy.ws(req, socket, head)
})
server.listen(3000)
Dynamic routing
import httpProxy from "http-proxy"
import http from "node:http"
const proxy = httpProxy.createProxyServer()
const routes: Record<string, string> = {
"/api/packages": "http://package-service:3001",
"/api/users": "http://user-service:3002",
"/api/billing": "http://billing-service:3003",
}
const server = http.createServer((req, res) => {
// Find matching route:
const route = Object.keys(routes).find(prefix => req.url?.startsWith(prefix))
if (route) {
proxy.web(req, res, { target: routes[route] })
} else {
res.writeHead(404)
res.end("Not found")
}
})
server.listen(3000)
Load balancing
import httpProxy from "http-proxy"
import http from "node:http"
const targets = [
"http://app-1:3000",
"http://app-2:3000",
"http://app-3:3000",
]
let current = 0
const proxy = httpProxy.createProxyServer()
const server = http.createServer((req, res) => {
// Round-robin:
const target = targets[current % targets.length]
current++
proxy.web(req, res, { target })
})
server.listen(80)
SSL termination
import httpProxy from "http-proxy"
import https from "node:https"
import fs from "node:fs"
const proxy = httpProxy.createProxyServer({
target: "http://localhost:8080", // Backend is HTTP
changeOrigin: true,
})
// HTTPS frontend → HTTP backend:
const server = https.createServer({
key: fs.readFileSync("certs/key.pem"),
cert: fs.readFileSync("certs/cert.pem"),
}, (req, res) => {
proxy.web(req, res)
})
server.listen(443)
@fastify/http-proxy
@fastify/http-proxy — Fastify proxy plugin:
Basic setup
import Fastify from "fastify"
import proxy from "@fastify/http-proxy"
const fastify = Fastify({ logger: true })
// Proxy /api/* to backend:
await fastify.register(proxy, {
upstream: "http://localhost:8080",
prefix: "/api",
rewritePrefix: "/", // /api/packages → /packages
})
await fastify.listen({ port: 3000 })
Multiple upstreams (API gateway)
import Fastify from "fastify"
import proxy from "@fastify/http-proxy"
const fastify = Fastify({ logger: true })
// Package service:
await fastify.register(proxy, {
upstream: "http://package-service:3001",
prefix: "/api/packages",
rewritePrefix: "/packages",
})
// User service:
await fastify.register(proxy, {
upstream: "http://user-service:3002",
prefix: "/api/users",
rewritePrefix: "/users",
})
// Billing service:
await fastify.register(proxy, {
upstream: "http://billing-service:3003",
prefix: "/api/billing",
rewritePrefix: "/billing",
})
await fastify.listen({ port: 3000 })
With Fastify hooks (auth, logging)
await fastify.register(proxy, {
upstream: "http://package-service:3001",
prefix: "/api/packages",
// Pre-handler — runs before proxying:
preHandler: async (request, reply) => {
// Authenticate:
const token = request.headers.authorization
if (!token) {
return reply.code(401).send({ error: "Unauthorized" })
}
// Add request ID:
request.headers["x-request-id"] = crypto.randomUUID()
},
// Modify reply headers:
replyOptions: {
onResponse(request, reply, res) {
reply.header("x-proxy", "pkgpulse-gateway")
reply.send(res)
},
},
})
WebSocket proxying
await fastify.register(proxy, {
upstream: "http://localhost:8080",
prefix: "/ws",
websocket: true, // Enable WebSocket proxying
})
Feature Comparison
| Feature | http-proxy-middleware | node-http-proxy | @fastify/http-proxy |
|---|---|---|---|
| Framework | Express/Connect | Any | Fastify only |
| Underlying engine | http-proxy | Self | undici |
| Path rewriting | ✅ | Manual | ✅ (rewritePrefix) |
| WebSocket proxy | ✅ | ✅ | ✅ |
| Request modification | ✅ (onProxyReq) | ✅ (proxyReq event) | ✅ (preHandler) |
| Load balancing | ❌ | Manual | ❌ (use with fastify-reply-from) |
| SSL termination | ❌ | ✅ | ❌ |
| Performance | Good | Good | Best (undici) |
| Weekly downloads | ~8M | ~5M | ~200K |
When to Use Each
Choose http-proxy-middleware if:
- Express application — industry standard proxy middleware
- Dev server proxying (avoiding CORS during development)
- Simple API gateway with path rewriting
- Using Vite/CRA dev proxy (uses this under the hood)
Choose node-http-proxy if:
- Building a standalone proxy server (not inside an app framework)
- Need SSL termination, load balancing, or custom routing logic
- Want maximum control over the proxy behavior
- Implementing A/B testing or canary deployments at the proxy level
Choose @fastify/http-proxy if:
- Building an API gateway with Fastify
- Need Fastify's hook system for auth, logging, rate limiting
- Want undici's performance for high-throughput proxying
- Microservice architecture with Fastify
Methodology
Download data from npm registry (weekly average, February 2026). Feature comparison based on http-proxy-middleware v3.x, http-proxy v1.x, and @fastify/http-proxy v10.x.
Compare proxy, API gateway, and networking packages on PkgPulse →