The Hardened Front Door in Front of Your Node App
Reverse Proxies
What a reverse proxy does — TLS termination, compression, static serving, routing — with nginx in front of Node, and how it differs from a load balancer and an API gateway.
What you'll learn
- List the jobs a reverse proxy offloads from your app servers
- Configure nginx in front of a Node app at a sketch level
- Distinguish reverse proxy, load balancer, and API gateway
A reverse proxy is a server that sits in front of your app servers and forwards client requests to them. “Reverse” because a normal (forward) proxy acts on behalf of clients reaching out to the internet; a reverse proxy acts on behalf of servers receiving traffic. To the outside world it is your service — clients never talk to your Node processes directly.
That layer of indirection is where you offload all the cross-cutting work that has nothing to do with your business logic.
What a reverse proxy does for you
- TLS termination. It holds your certificates and does the expensive HTTPS handshake and decryption, then talks plain HTTP to your app on the trusted internal network. Your Node code never touches certs.
- Compression. It gzips/brotli-compresses responses on the way out, shrinking payloads without your app spending event-loop time on it.
- Serving static assets. It serves
/assets/*.js, images, and CSS straight from disk — orders of magnitude faster than routing those through Node, and it keeps your app process free for dynamic work. - Routing. It maps paths to upstreams:
/api→ the Node pool,/→ a static SPA bundle,/grafana→ an internal dashboard. - Buffering & slow-client protection. It absorbs slow or trickling clients so a single slow connection can’t tie up a precious Node worker.
- Caching, rate limiting, and basic security (request size limits, header filtering) at the edge, before traffic ever reaches your app.
A minimal nginx config in front of Node
Here’s the shape of an nginx server block that terminates TLS, serves static files directly, and reverse-proxies everything else to a local Node app:
# /etc/nginx/sites-available/app.conf
upstream node_app {
least_conn; # distribute across local Node workers
server 127.0.0.1:3001;
server 127.0.0.1:3002;
}
server {
listen 443 ssl http2;
server_name jsschools.com;
ssl_certificate /etc/letsencrypt/live/jsschools.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/jsschools.com/privkey.pem;
gzip on;
gzip_types text/css application/javascript application/json;
# Serve hashed static assets straight from disk, cached hard.
location /assets/ {
root /var/www/app/public;
expires 1y;
add_header Cache-Control "public, immutable";
}
# Everything else → the Node app over plain HTTP.
location / {
proxy_pass http://node_app;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
# Redirect plain HTTP to HTTPS.
server {
listen 80;
server_name jsschools.com;
return 301 https://$host$request_uri;
}
The X-Forwarded-* headers matter: because the proxy terminated TLS and made
the connection, your Node app would otherwise think every request came from
127.0.0.1 over HTTP. Those headers carry the real client IP and original
protocol forward — and your app has to be told to trust them.
import express from 'express';
const app = express();
// Tell Express it's behind exactly one trusted proxy (nginx).
// Now req.ip and req.protocol read the X-Forwarded-* headers
// instead of reporting the proxy's own address.
app.set('trust proxy', 1);
app.get('/whoami', (req, res) => {
res.json({
ip: req.ip, // the real client IP, via X-Forwarded-For
secure: req.secure, // true — TLS was terminated upstream
proto: req.protocol, // 'https', via X-Forwarded-Proto
});
});
app.listen(3001); Reverse proxy vs load balancer vs API gateway
These three overlap heavily — the same software (nginx, Envoy) can play all three roles — but they’re distinct jobs:
| Role | Core job | Operates at |
|---|---|---|
| Reverse proxy | Front your servers; offload TLS, compression, static, routing | Connection / HTTP |
| Load balancer | Spread traffic across a pool, with health checks | L4 or L7 |
| API gateway | App-aware edge: auth, rate limiting, API keys, request shaping, aggregation | HTTP / API |
The mental model: a load balancer answers “which server?”, a reverse proxy answers “what general front-door work happens before the app?”, and an API gateway answers “is this caller allowed, and what should their request become?” Most L7 load balancers are reverse proxies (they terminate and forward HTTP). An API gateway is a reverse proxy that’s been taught about your API — authentication, quotas, key management, and per-route policy. We give the API gateway its own deep dive later in the track.
For now, the takeaway: put a reverse proxy in front of every Node service. It hardens the front door and keeps your single-threaded app focused on the one thing only it can do — run your code. Speaking of what your app should not hold onto: next we tackle why in-process state quietly breaks all of this.