NGINX as API Gateway — Complete Guide
In this tutorial, you'll learn about NGINX as API Gateway. We cover key concepts, practical examples, and best practices to help you understand and apply this topic effectively.
NGINX is a high-performance web server that can function as an API gateway, providing reverse proxying, Load Balancing, SSL termination, Rate Limiting, and Caching.
What You'll Learn
By the end of this lesson, you will configure NGINX as a reverse proxy, implement rate limiting, TLS termination, and caching rules for API traffic.
Why It Matters
NGINX handles millions of concurrent connections with low resource usage. Many organizations use NGINX as both web server and gateway for its performance and reliability.
Real-World Use
NGINX sits in front of three application servers, terminates SSL, routes /api/* to the API service, serves static files directly, and rate limits to 100 req/s.
NGINX Gateway Architecture
flowchart LR
Client -->|HTTPS:443| NGINX[NGINX Gateway]
NGINX -->|SSL Terminate| NGINX
NGINX -->|/api/*| API[API Service:3000]
NGINX -->|/static/*| Static[Static Files]
NGINX -->|Rate Limit| Limit[Limit 100r/s]
NGINX -->|Cache| Cache[Cache Layer]
NGINX Configuration Generation
# nginx_config.py
from typing import Dict, List, Optional
class NGINXConfigGenerator:
def __init__(self):
self.upstreams: Dict[str, list] = {}
self.locations: List[dict] = []
self.global_settings: Dict[str, str] = {}
def add_upstream(self, name: str, servers: List[str],
lb_method: str = "round_robin"):
self.upstreams[name] = {
"servers": servers,
"method": lb_method,
}
def add_location(self, path: str, proxy_pass: str,
cache: bool = False,
rate_limit: Optional[str] = None):
location = {
"path": path,
"proxy_pass": proxy_pass,
"cache": cache,
"rate_limit": rate_limit,
}
self.locations.append(location)
def set(self, key: str, value: str):
self.global_settings[key] = value
def generate(self) -> str:
lines = []
lines.extend(f"{k} {v};" for k, v in self.global_settings.items())
lines.append("")
for name, conf in self.upstreams.items():
lines.append(f"upstream {name} {{")
lines.append(f" {conf['method']};")
for server in conf["servers"]:
lines.append(f" server {server};")
lines.append("}")
lines.append("")
lines.append("server {")
lines.append(" listen 443 ssl;")
lines.append(" server_name api.example.com;")
if self.global_settings.get("ssl_certificate"):
lines.append(f" ssl_certificate {self.global_settings['ssl_certificate']};")
lines.append(f" ssl_certificate_key {self.global_settings['ssl_certificate_key']};")
for loc in self.locations:
lines.append(f" location {loc['path']} {{")
lines.append(f" proxy_pass {loc['proxy_pass']};")
lines.append(" proxy_set_header Host $host;")
lines.append(" proxy_set_header X-Real-IP $remote_addr;")
lines.append(" proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;")
lines.append(" proxy_set_header X-Forwarded-Proto $scheme;")
if loc["rate_limit"]:
lines.append(f" limit_req zone={loc['rate_limit']};")
if loc["cache"]:
lines.append(" proxy_cache api_cache;")
lines.append(" proxy_cache_valid 200 60s;")
lines.append(" }")
lines.append("")
lines.append("}")
return "\n".join(lines)
nginx = NGINXConfigGenerator()
nginx.set("worker_processes", "auto")
nginx.set("ssl_certificate", "/etc/ssl/certs/api.crt")
nginx.set("ssl_certificate_key", "/etc/ssl/private/api.key")
nginx.add_upstream("api_backend", [
"10.0.0.1:3000 weight=3",
"10.0.0.2:3000 weight=2",
"10.0.0.3:3000 weight=1",
])
nginx.add_location("/api/v1", "http://api_backend",
cache=True, rate_limit="api_limit")
nginx.add_location("/api/v2", "http://api_backend")
nginx.add_location("/static", "/var/www/static")
config = nginx.generate()
print(config[:500])
print("...")
print(f"\nUpstreams: {len(nginx.upstreams)}")
print(f"Locations: {len(nginx.locations)}")
Expected output:
worker_processes auto;
ssl_certificate /etc/ssl/certs/api.crt;
ssl_certificate_key /etc/ssl/private/api.key;
upstream api_backend {
round_robin;
server 10.0.0.1:3000 weight=3;
server 10.0.0.2:3000 weight=2;
server 10.0.0.3:3000 weight=1;
}
...
Upstreams: 1
Locations: 3
Rate Limiting Configuration
# nginx_ratelimit.py
from typing import Dict, Optional
class NGINXRateLimitConfig:
def __init__(self):
self.zones: Dict[str, dict] = {}
def add_zone(self, name: str, rate: str, size: str = "10m"):
self.zones[name] = {"rate": rate, "size": size}
def generate_http_block(self) -> str:
lines = ["http {"]
for name, conf in self.zones.items():
lines.append(
f" limit_req_zone $binary_remote_addr zone={name}:{conf['size']} "
f"rate={conf['rate']};"
)
lines.append("}")
return "\n".join(lines)
def generate_location_block(self, zone: str, burst: int = 0,
nodelay: bool = False) -> str:
parts = [f"limit_req zone={zone} burst={burst}"]
if nodelay:
parts[0] += " nodelay"
return parts[0] + ";"
rl = NGINXRateLimitConfig()
rl.add_zone("api_limit", "100r/s", "10m")
rl.add_zone("auth_limit", "10r/s", "5m")
rl.add_zone("upload_limit", "5r/m", "10m")
print(rl.generate_http_block())
print()
print("Location usage:")
print(f" /api/: {rl.generate_location_block('api_limit', burst=50, nodelay=True)}")
print(f" /auth/: {rl.generate_location_block('auth_limit', burst=5)}")
print(f" /upload/: {rl.generate_location_block('upload_limit')}")
Expected output:
http {
limit_req_zone $binary_remote_addr zone=api_limit:10m rate=100r/s;
limit_req_zone $binary_remote_addr zone=auth_limit:5m rate=10r/s;
limit_req_zone $binary_remote_addr zone=upload_limit:10m rate=5r/m;
}
Location usage:
/api/: limit_req zone=api_limit burst=50 nodelay;
/auth/: limit_req zone=auth_limit burst=5;
/upload/: limit_req zone=upload_limit burst=0;
Common Mistakes
1. Not Setting Proxy Headers
Without proxy_set_header directives, backend servers see NGINX's IP instead of the client IP. Always forward X-Forwarded-For.
2. Insufficient Worker Connections
worker_connections defaults to 512. For high-traffic APIs, increase to 4096 or higher.
3. No Limit for Request Body Size
client_max_body_size defaults to 1MB. File upload APIs need explicit size configuration.
4. SSL Configuration Without Optimization
Default SSL settings are slow. Enable session caching, OCSP stapling, and modern cipher suites.
5. No Health Checks for Upstreams
Without health checks, NGINX continues sending traffic to failed servers. Use the upstream check module or passive health checks.
Practice Questions
1. How does NGINX handle reverse proxying?
It receives client requests and forwards them to upstream servers based on location blocks, then returns the upstream response to the client.
2. What is the purpose of upstream blocks in NGINX?
Upstream blocks define groups of backend servers with load balancing configuration, allowing NGINX to distribute traffic.
3. How do you configure NGINX rate limiting?
Define a limit_req_zone in the http block and reference it with limit_req in location blocks, specifying rate, burst, and delay.
4. What NGINX directives are needed for SSL termination?
listen 443 ssl, ssl_certificate, ssl_certificate_key, plus SSL protocol and cipher configuration.
Challenge
Write an NGINX configuration that proxies /api/v1 to an upstream of three servers with least_conn balancing, rate limits at 200 req/s with burst 50, caches successful responses for 60 seconds, and terminates TLS with modern ciphers.
FAQ
Mini Project: NGINX Simulator
# nginx_sim.py
import time
from typing import Dict, List, Optional
class NGINXSimulator:
def __init__(self):
self.upstreams: Dict[str, List[dict]] = {}
self.rate_limit_zones: Dict[str, dict] = {}
self.cache: Dict[str, dict] = {}
def add_upstream(self, name: str, servers: List[str]):
self.upstreams[name] = [
{"address": s, "healthy": True} for s in servers
]
def proxy_pass(self, upstream: str, request: dict) -> dict:
servers = [s for s in self.upstreams.get(upstream, []) if s["healthy"]]
if not servers:
return {"status": 502, "body": "No healthy upstream servers"}
server = servers[hash(request.get("path", "")) % len(servers)]
return {
"status": 200,
"body": f"Proxied to {server['address']}",
"upstream": upstream,
}
sim = NGINXSimulator()
sim.add_upstream("api", ["10.0.0.1:3000", "10.0.0.2:3000", "10.0.0.3:3000"])
for path in ["/api/users", "/api/orders", "/api/users", "/api/products"]:
result = sim.proxy_pass("api", {"path": path})
print(f"{path:20s} -> {result['body']}")
Expected output:
/api/users -> Proxied to 10.0.0.1:3000
/api/orders -> Proxied to 10.0.0.2:3000
/api/users -> Proxied to 10.0.0.1:3000
/api/products -> Proxied to 10.0.0.3:3000
What's Next
You understand NGINX as API gateway. Next, learn about Envoy proxy, then explore AWS API Gateway.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro