Skip to content

NGINX as API Gateway — Complete Guide

DodaTech Updated 2026-06-28 5 min read

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

Is NGINX a full API gateway?

NGINX is primarily a reverse proxy and web server. It can function as a basic gateway but lacks advanced features like API key management found in dedicated gateways.

Can NGINX handle WebSockets?

Yes. NGINX supports WebSocket proxying with the Upgrade header forwarding configuration.

How does NGINX compare to Kong?

Kong is built on NGINX but adds a plugin system, admin API, and database-backed configuration. NGINX is lower-level.

Can NGINX cache API responses?

Yes. NGINX has a built-in caching system using proxy_cache with configurable cache keys and TTLs.

Is NGINX Plus needed for advanced features?

NGINX Plus adds health checks, session persistence, and API gateway features. The open-source version covers most reverse proxy needs.

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