Build a Dynamic Reverse Proxy with Node.js

This article explains how to build and configure a dynamic reverse proxy using Node.js to intercept, rewrite, and route incoming HTTP requests. You will learn how to set up a lightweight proxy server, define dynamic routing rules based on request paths or headers, and rewrite request URLs on the fly before forwarding them to backend services.

Understanding the Concept

A reverse proxy acts as an intermediary between client devices and backend servers. While a standard proxy routes requests based on static configurations, a dynamic reverse proxy inspects incoming requests and programmatically alters their destination or path in real-time. This is highly useful for microservices architectures, API versioning, load balancing, and A/B testing.

Prerequisites and Setup

To implement this solution, you need Node.js installed on your machine. Start by initializing a new Node.js project and installing the http-proxy package, which simplifies request forwarding.

mkdir node-dynamic-proxy
cd node-dynamic-proxy
npm init -y
npm install http-proxy

Step-by-Step Implementation

The core of a dynamic reverse proxy consists of an HTTP server that evaluates the req.url or headers, modifies them, and forwards the payload to a target destination using the http-proxy library.

Create a file named proxy.js and add the following code:

const http = require('http');
const httpProxy = require('http-proxy');

// Create the proxy server instance
const proxy = httpProxy.createProxyServer({});

// Handle proxy errors to prevent the process from crashing
proxy.on('error', (err, req, res) => {
  console.error('Proxy Error:', err);
  res.writeHead(502, { 'Content-Type': 'text/plain' });
  res.end('Bad Gateway: The target server is unreachable.');
});

// Create the main HTTP server
const server = http.createServer((req, res) => {
  let target = 'http://localhost:3000'; // Default fallback target

  // Dynamic Routing and Path Rewriting Logic
  if (req.url.startsWith('/api/v1')) {
    // Rewrite path: /api/v1/users -> /legacy/users
    req.url = req.url.replace('/api/v1', '/legacy');
    target = 'http://localhost:4001'; 
  } else if (req.url.startsWith('/api/v2')) {
    // Rewrite path: /api/v2/users -> /v2/users
    req.url = req.url.replace('/api/v2', '/v2');
    target = 'http://localhost:4002';
  } else if (req.headers['x-use-beta'] === 'true') {
    // Route based on request headers
    target = 'http://localhost:5000';
  }

  console.log(`Proxying ${req.method} ${req.url} to -> ${target}`);

  // Forward the request to the dynamically selected target
  proxy.web(req, res, { target: target, changeOrigin: true });
});

// Start the proxy server
const PORT = 8080;
server.listen(PORT, () => {
  console.log(`Dynamic Reverse Proxy running on port ${PORT}`);
});

How the Code Works

  1. Proxy Instantiation: httpProxy.createProxyServer({}) initializes the proxy agent that handles TCP socket streaming and response pipe management.
  2. Path and Header Inspection: Inside the http.createServer callback, the server reads the request properties (req.url and req.headers).
  3. Dynamic Path Rewriting: JavaScript’s String.prototype.replace() is used to dynamically modify the path structure before sending it to the backend. This ensures the target API receives the formatted path it expects.
  4. Target Redirection: The target variable is re-assigned conditionally depending on the rules matched.
  5. Request Forwarding: proxy.web(req, res, { target }) sends the modified request stream to the chosen destination and pipes the backend’s response back to the client.
  6. Error Handling: The proxy.on('error') event listener prevents the Node.js application from crashing if a backend server is offline, returning a 502 Bad Gateway status instead.