Nginx as API gateway cheatsheet.
Architecture
[clients] → nginx (TLS, auth, rate-limit, routing) → microservices
Or use Kong, KrakenD, Tyk, Ambassador. Nginx (+ OpenResty) is the simplest.
Path-based routing
upstream users { server users-svc:8000; }
upstream orders { server orders-svc:8000; }
upstream payments { server payments-svc:8000; }
server {
listen 443 ssl;
server_name api.example.com;
location /v1/users/ {
proxy_pass http://users/;
}
location /v1/orders/ {
proxy_pass http://orders/;
}
location /v1/payments/ {
proxy_pass http://payments/;
}
}
Header-based routing
map $http_x_service $upstream {
"users" users;
"orders" orders;
default backend;
}
location / {
proxy_pass http://$upstream;
}
Auth (forward to auth service)
location /v1/ {
auth_request /_auth;
auth_request_set $user $upstream_http_x_user_id;
proxy_set_header X-User-ID $user;
proxy_pass http://backend;
}
location = /_auth {
internal;
proxy_pass http://auth-svc/verify;
proxy_pass_request_body off;
proxy_set_header Content-Length "";
proxy_set_header Authorization $http_authorization;
}
/_auth returns 200 + X-User-ID header if valid; 401 if not.
JWT validation (OpenResty)
location /v1/ {
access_by_lua_block {
local jwt = require("resty.jwt")
local token = ngx.var.http_authorization
if not token then return ngx.exit(401) end
local obj = jwt:verify("secret", token:gsub("Bearer ", ""))
if not obj.verified then return ngx.exit(403) end
ngx.req.set_header("X-User-ID", obj.payload.sub)
}
proxy_pass http://backend;
}
Rate limiting per endpoint
limit_req_zone $binary_remote_addr zone=login:10m rate=5r/m;
limit_req_zone $binary_remote_addr zone=api:10m rate=100r/s;
location /v1/login {
limit_req zone=login burst=2 nodelay;
proxy_pass http://auth;
}
location /v1/ {
limit_req zone=api burst=200 nodelay;
proxy_pass http://backend;
}
CORS
location /v1/ {
if ($request_method = OPTIONS) {
add_header Access-Control-Allow-Origin "https://app.example.com" always;
add_header Access-Control-Allow-Methods "GET, POST, PUT, DELETE";
add_header Access-Control-Allow-Headers "Content-Type, Authorization";
add_header Access-Control-Max-Age 86400;
return 204;
}
add_header Access-Control-Allow-Origin "https://app.example.com" always;
add_header Access-Control-Allow-Credentials "true" always;
proxy_pass http://backend;
}
Request transformation (OpenResty)
location /v1/legacy/ {
rewrite_by_lua_block {
ngx.req.set_uri(ngx.var.request_uri:gsub("/v1/legacy/", "/old/"))
ngx.req.set_header("X-API-Version", "1")
}
proxy_pass http://legacy;
}
Response transformation
location /v1/users {
proxy_pass http://users;
body_filter_by_lua_block {
local chunk = ngx.arg[1]
if chunk and chunk ~= "" then
-- inject field, rename, etc
ngx.arg[1] = chunk:gsub('"email"', '"emailAddress"')
end
}
}
API versioning
location /v1/ { proxy_pass http://app_v1; }
location /v2/ { proxy_pass http://app_v2; }
Or by Accept header:
map $http_accept $version {
~application/vnd.example.v2 v2;
default v1;
}
map $version $upstream {
v1 app_v1;
v2 app_v2;
}
Aggregation (multiple backends)
Use OpenResty ngx.location.capture_multi or move to a real BFF / GraphQL service.
Logging per service
log_format svc '$remote_addr $request_method $uri $status $request_time service=$service';
location /v1/users { set $service "users"; access_log /var/log/nginx/users.log svc; ... }
location /v1/orders { set $service "orders"; access_log /var/log/nginx/orders.log svc; ... }
Per-service file or single file with $service field.
Circuit breaker (simple)
proxy_next_upstream error timeout http_502 http_503 http_504;
proxy_next_upstream_tries 2;
proxy_next_upstream_timeout 5s;
upstream backend {
server 10.0.0.1 max_fails=5 fail_timeout=30s;
server 10.0.0.2 max_fails=5 fail_timeout=30s;
}
For richer: use service mesh.
API docs
location /docs/ {
alias /var/www/docs/;
autoindex off;
}
Serve Swagger UI / Redoc.
Maintenance mode
location / {
if (-f /etc/nginx/maintenance.flag) {
return 503;
}
proxy_pass http://backend;
}
error_page 503 @maintenance;
location @maintenance {
root /var/www/errors;
try_files /503.html =503;
}
touch /etc/nginx/maintenance.flag to enable.
Common mistakes
- Auth at gateway only → service trusts forwarded headers without checking signature.
- Forgetting CORS preflight (OPTIONS).
- Rate limit per IP behind LB without real_ip → all hit same IP.
- Transforming responses without thinking about chunked / compression.
- One large nginx vs multiple specialized ones — favor multiple.
Read this next
If you want my API gateway template, it’s at rajpoot.dev .
Building something AI-, backend-, or data-heavy and want a second pair of eyes? I do consulting and freelance work — see my projects and ways to reach me at rajpoot.dev .