Like many Node.js developers, I started by running my app directly with node app.js. Then I learned about production deployments, and someone mentioned "You should put NGINX in front of your Node app." š But why? My app was working fine!š
The Evolution
First Step: Reverse Proxy
Started with the basics - using NGINX as a reverse proxy:
- NGINX handles SSL/HTTPS termination cleanly
- Serves static files (images, CSS, JS) much faster than Node.js
- Adds an extra security layer by hiding our Node.js server
# Basic reverse proxy setup
server {
listen 80;
server_name myapp.com;
# Forward to Node.js
location / {
proxy_pass http://localhost:3000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_cache_bypass $http_upgrade;
}
}
Level Up: API Gateway
Then came the exciting part - routing different backend servers through path-based routing. I had two clients(Client A and Client B), and for each I have created a separate node.js backend but the server is same.
Lets suppose the backend server domain is api.backend.com
.
- Client A backend runs on
http://localhost:3000
and serves requests like/api/someEndpoint
- Client B backend runs on
http://localhost:4000
and serves requests like/api/anotherEndpoint
I have want to setup it like this:
The frontend makes requests to:
https://api.backend.com/api/clientA/someEndpoint
ā This should go tohttp://localhost:3000/api/someEndpoint
https://api.backend.com/api/clientB/anotherEndpoint
ā This should go tohttp://localhost:4000/api/anotherEndpoint
So, I setup the NGINX like this:
server {
listen 80;
server_name myapp.com;
# Backend API for Client A
location /api/clientA {
# Remove the /api/clientA prefix before proxying
rewrite ^/api/clientA/(.*) /api/$1 break;
proxy_pass http://localhost:3000; # Client A's backend
proxy_http_version 1.1;
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_cache_bypass $http_upgrade;
}
# Backend API for Client B
location /api/clientB {
# Remove the /api/clientB prefix before proxying
rewrite ^/api/clientB/(.*) /api/$1 break;
proxy_pass http://localhost:4000; # Client B's backend
proxy_http_version 1.1;
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_cache_bypass $http_upgrade;
}
# Optional: Default fallback for other unmatched requests
location / {
return 404 '{"error": "Not found"}';
default_type application/json;
}
}
Requests and Redirection Explanation
Frontend Request to Client A
API:
- Frontend hits:
https://api.backend.com/api/clientA/someEndpoint
- Nginx rewrites the URL: /api/clientA/someEndpoint ā /api/someEndpoint
- Nginx forwards the request to:
http://localhost:3000/api/someEndpoint
- Response: Nginx proxies the response back to the frontend.
Frontend Request to Client B
API:
- Frontend hits:
https://api.backend.com/api/clientB/anotherEndpoint
- Nginx rewrites the URL: /api/clientB/anotherEndpoint ā /api/anotherEndpoint
- Nginx forwards the request to:
http://localhost:4000/api/anotherEndpoint
- Response: Nginx proxies the response back to the frontend.
Unmatched Request:
- Frontend hits:
https://api.backend.com/someRandomPath
- Nginx responds with: 404 Not Found and a JSON error message.
Note: The frontend does not need to know about the internal backend ports (3000, 4000). Nginx abstracts this, so the frontend communicates with a single domain (api.backend.com) and Nginx routes the traffic to the appropriate backend.
A Security Move: Rate Limiting
As traffic grew, I needed to protect my APIs from abuse. So that no one can spam my APIs.
# Define a rate limit zone for the API
limit_req_zone $binary_remote_addr zone=api_limit:10m rate=15r/s;
server {
listen 80;
server_name myapp.com;
# Apply rate limiting to all API traffic
location /api {
# Rate limit: 15 requests per second with burst of 10, no delay
limit_req zone=api_limit burst=10 nodelay;
error_page 429 =429 '{"error": "Too many requests. Please try again later."}';
default_type application/json;
proxy_pass http://localhost:3000; # Single Backend server
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}
Explanation:
- Rate: 15 requests per second per IP.
- Burst: Up to 10 additional requests allowed for short spikes.
- Error Handling: Returns a 429 Too Many Requests response with a JSON error message.
- Backend: All /api traffic is proxied to a single Node.js backend
(http://localhost:3000)
.
If you have multiple backend servers, you can setup rate limiting for each backend server.
Multiple Servers with Individual Rate Limits
This configuration is suitable for scenarios where different backend APIs (e.g., Client A and Client B) have separate rate limits.
# Define a shared rate limit zone for API traffic
limit_req_zone $binary_remote_addr zone=clientA_limit:10m rate=15r/s;
limit_req_zone $binary_remote_addr zone=clientB_limit:10m rate=10r/s;
server {
listen 80;
server_name myapp.com;
# Rate limiting for Client A
location /api/clientA {
# 15 requests/second, burst of 10
limit_req zone=clientA_limit burst=10 nodelay;
error_page 429 =429 '{"error": "Too many requests for Client A. Please try again later."}';
default_type application/json;
rewrite ^/api/clientA/(.*) /api/$1 break;
proxy_pass http://localhost:3000; # Client A backend
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
# Rate limiting for Client B
location /api/clientB {
# 10 requests/second, burst of 5
limit_req zone=clientB_limit burst=5 nodelay;
error_page 429 =429 '{"error": "Too many requests for Client B. Please try again later."}';
default_type application/json;
rewrite ^/api/clientB/(.*) /api/$1 break;
proxy_pass http://localhost:4000; # Client B backend
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}
Explanation:
- Client A: 15 requests/second, 10 bursts allowed.
- Client B: 10 requests/second, 5 bursts allowed.
- Custom Error Responses: Different messages for rate limiting violations on Client A and Client B.
Comparison
Feature | Single Server Setup | Multiple Servers Setup |
---|---|---|
Rate Limit Zone | api_limit shared for all traffic | Separate zones (clientA_limit, clientB_limit) for each backend |
Burst Limit | Same for all traffic | Different burst limits per backend |
Error Response | Generic for all requests | Custom per backend |
Backend Servers | Single backend (http://localhost:3000) | Separate backends for Client A and Client B |
Complexity | Simpler to configure and manage | More granular control, slightly complex |
Important Lesson
- You cannot just copy paste the nginx config from one project to another. You need to understand the project requirements and then set it up.
- Most of the configurations are common and can be reused but you need to understand the flow so that you can update it accordingly.
- NGINX isn't just a web server - it's a powerful tool for managing your web infrastructure
- Path-based routing makes it easy to manage multiple applications under one domain
- Rate limiting is crucial for protecting your APIs from abuse
- Different clients might need different rate limits
- Always plan for traffic spikes with proper burst configurations
- Custom error responses improves user experience