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 .