CodingNic

Reverse Proxies & HTTPS

What a Reverse Proxy Does

Reverse Proxies & HTTPS 15 min read

What a Reverse Proxy Does

Objectives

By the end of this lesson, you should be able to:

  • Explain what a reverse proxy sits between, and why
  • Build a working reverse proxy in Node.js, forwarding requests to a backend
  • Explain what headers a reverse proxy adds, and why a backend needs to trust them carefully

💡 Why this matters: Every request in this course, and Course 3, has gone straight to Express. Real production traffic almost never does, a reverse proxy sits in front, and this lesson builds one directly, to see exactly what it actually does to a request along the way.

⚠️ A note on verification: every snippet and every output in this lesson was actually run.

What Sits Where

A reverse proxy sits between the outside world and an application’s backend, every incoming request hits the proxy first, which then forwards it to the right backend, and forwards the response back. This is the opposite of a forward proxy, which sits in front of clients, hiding them from servers, a reverse proxy hides servers from clients instead, a client only ever sees the proxy’s address, never the backend directly.

A Minimal Reverse Proxy

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

const proxy = httpProxy.createProxyServer({});

const server = http.createServer((req, res) => {
  proxy.web(req, res, {
    target: 'http://localhost:7001',
    xfwd: true
  });
});

server.listen(7000, () => console.log('Reverse proxy on 7000'));

Every request to port 7000 gets forwarded to whatever’s running on port 7001, xfwd: true tells the proxy to add forwarding headers, covered next, this is a genuinely working reverse proxy, in about ten lines.

Comparing a Direct Request to a Proxied One

javascript
app.get('/api/v1/health', (req, res) => {
  res.json({
    status: 'ok',
    seenHost: req.headers.host,
    seenForwardedFor: req.headers['x-forwarded-for'],
    seenForwardedProto: req.headers['x-forwarded-proto']
  });
});
text
--- request straight to backend ---
{ status: 'ok', seenHost: 'localhost:7001' }

--- request through the reverse proxy ---
{
  status: 'ok',
  seenHost: 'localhost:7000',
  seenForwardedFor: '::ffff:127.0.0.1',
  seenForwardedProto: 'http'
}

Requested directly, the backend sees no forwarding headers at all, requested through the proxy, it sees x-forwarded-for (the original client’s address) and x-forwarded-proto (the original protocol, http or https), and host itself changes, to the proxy’s own address, not the backend’s, exactly what “hides servers from clients” looks like from the backend’s own point of view.

Why a Backend Needs to Trust These Headers Carefully

X-Forwarded-For is genuinely useful, a backend behind a proxy needs it to know a request’s real originating address, for logging (Course 3, Module 9), or rate limiting (Course 3, Module 6) by real client, not by the proxy’s own address, which every request would otherwise share. But it’s also just a header, anyone could set it directly on a request sent straight to an unprotected backend, a production deployment needs to ensure the backend is only reachable through the trusted proxy, never directly, otherwise X-Forwarded-For becomes trivially spoofable, undermining exactly the logging and rate-limiting decisions built on top of it.

Routing by Path

javascript
const server = http.createServer((req, res) => {
  if (req.url.startsWith('/admin')) {
    req.url = req.url.replace(/^\/admin/, '');
    proxy.web(req, res, { target: 'http://localhost:7003' });
  } else if (req.url.startsWith('/api')) {
    req.url = req.url.replace(/^\/api/, '');
    proxy.web(req, res, { target: 'http://localhost:7002' });
  } else {
    res.writeHead(404);
    res.end('Not found');
  }
});
text
/api/status -> { service: 'api-backend' }
/admin/status -> { service: 'admin-backend' }

One reverse proxy, one public entry point, routing to two entirely separate backend services based on the request path, a client never needs to know, or care, that /api and /admin are served by different applications entirely, this is a real, common reverse proxy pattern, not just a forwarding pass-through.

Try It

  1. Build the minimal reverse proxy and backend above, and confirm the forwarding headers appear only when going through the proxy.
  2. Build the path-based routing proxy, with two separate backend services, and confirm each path reaches the correct one.
  3. Explain, in your own words, why a backend should never be directly reachable from outside, once it’s meant to sit behind a reverse proxy.
  4. Explain, in one or two sentences, a real use for X-Forwarded-For in an application that’s already built rate limiting (Course 3, Module 6).

Recap

  • A reverse proxy sits between clients and backend servers, forwarding requests and hiding the backend’s real address entirely.
  • It adds forwarding headers, X-Forwarded-For, X-Forwarded-Proto, that a backend needs, but should only trust when the backend is genuinely unreachable except through the proxy.
  • A single reverse proxy can route to multiple backend services based on the request path, a real, common pattern, not just simple forwarding.

Next lesson: Nginx, the production-grade reverse proxy most real Node.js deployments actually run behind.