Skip to content

Repository files navigation

REST API Gateway

Production-oriented API gateway built with FastAPI. It sits in front of backend services as a reverse proxy, adding JWT and API-key authentication, token-bucket rate limiting (per client IP, with optional Redis for multi-instance deployments), structured access logging, Prometheus metrics, a small circuit breaker for upstream failures, and consistent JSON error responses.

Architecture

flowchart LR
  Client([Client]) --> GW[API Gateway]
  GW --> RL[Rate limit]
  RL --> Auth[Auth]
  Auth --> Proxy[Reverse proxy]
  Proxy --> B1[Backend A]
  Proxy --> B2[Backend B]
  GW -.-> Redis[(Optional Redis)]
  RL -.-> Redis
  GW --> Prom["/metrics"]
Loading

Request flow (simplified):

  1. Request context — Assign (or accept) X-Request-ID, emit structured JSON access logs.
  2. Rate limiting — Per-IP token bucket (in-process or Redis-backed).
  3. Authentication — Public paths bypass auth; otherwise require Authorization: Bearer <JWT> or X-API-Key when API keys are configured.
  4. Proxy — Longest-prefix route match, forward method, path, query string, headers, and body to the upstream; stream responses when appropriate.

Features

Area Details
Auth HS256 JWT (python-jose) with optional audience/issuer; API keys via X-API-Key
Rate limit Token bucket per IP (or X-Forwarded-For first hop); RATE_LIMIT_RPS, RATE_LIMIT_BURST, optional RATE_LIMIT_PER_MINUTE
Routing SERVICE_ROUTES_JSON prefix map + DEFAULT_BACKEND_URL; matched prefix stripped before forwarding
Resilience Configurable upstream timeout; simple circuit breaker after repeated failures
Observability JSON logs, X-Request-ID, Prometheus at /metrics
Errors Uniform JSON: {"error": {"code", "message", "request_id"?}}

Project layout

app/ # FastAPI app factory and lifespan
core/          # Settings, routing, token bucket, errors, circuit breaker
middleware/    # Logging, rate limit, authentication
routes/        # Proxy handler
sample_backend/# Demo upstream for Docker / local runs
observability/ # Prometheus scrape config + Grafana datasource provisioning
tests/         # Unit + integration tests

Prerequisites

  • Python 3.11+ (tested on 3.12 / 3.13)
  • pip and venv

Local setup (virtual environment)

From the repository root:

python -m venv .venv

Windows (PowerShell):

.\.venv\Scripts\Activate.ps1
pip install -r requirements.txt

Linux / macOS:

source .venv/bin/activate
pip install -r requirements.txt

Copy environment template and adjust secrets:

cp .env.example .env

Run the sample backend (optional, separate terminal):

pip install -r sample_backend/requirements.txt
cd sample_backend
uvicorn main:app --host 127.0.0.1 --port 9000

Start the gateway:

uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload

Open interactive docs at http://127.0.0.1:8000/docs (listed as a public path by default).

Configuration

All settings are driven by environment variables (see .env.example). Important variables:

Variable Purpose
JWT_SECRET Symmetric key for HS256 JWT validation
API_KEYS Comma-separated keys allowed via X-API-Key
PUBLIC_PATHS Comma-separated paths that skip auth and rate limiting
DEFAULT_BACKEND_URL Fallback upstream base URL
SERVICE_ROUTES_JSON JSON object: path prefix → upstream URL
RATE_LIMIT_RPS / RATE_LIMIT_BURST Token bucket refill rate and burst capacity
REDIS_URL If set, rate limits use a Redis-backed token bucket
PROXY_TIMEOUT_SECONDS Upstream HTTP timeout

Dynamic service routing

SERVICE_ROUTES_JSON is a JSON object whose keys are path prefixes (longest match wins). The gateway forwards to upstream_base + strip_prefix(original_path, matched_prefix).

Example:

SERVICE_ROUTES_JSON={"/api/users":"http://users:8080","/api/orders":"http://orders:8080"}
DEFAULT_BACKEND_URL=http://127.0.0.1:9000

A request to /api/users/42 is proxied to http://users:8080/42.

Authentication

JWT: Send Authorization: Bearer <token> signed with JWT_SECRET using HS256. Optional JWT_AUDIENCE / JWT_ISSUER enable standard claim checks.

API keys: When API_KEYS is non-empty, any listed key may be sent as X-API-Key: <secret> instead of a JWT.

Paths in PUBLIC_PATHS (and their subpaths, for entries like /health) skip authentication entirely.

Rate limiting

The gateway uses a token bucket per client identity:

  • Identity — First IP in X-Forwarded-For when present, otherwise the direct client IP.
  • ParametersRATE_LIMIT_RPS controls refill speed; RATE_LIMIT_BURST caps the maximum burst. If RATE_LIMIT_PER_MINUTE is set, the implementation raises the effective RPS floor to per_minute / 60 so a minute-level budget can be expressed.
  • Distributed mode — With REDIS_URL, token state is stored in Redis (Lua-backed atomic refill), so multiple gateway replicas share one limiter.

On exceed, the gateway returns 429 with JSON error.code = rate_limit_exceeded and a Retry-After: 1 header.

API usage examples

Health (no auth):

curl -s http://127.0.0.1:8000/health

Proxied request with JWT:

# Example only — issue a real token with your IdP or a small script using the same JWT_SECRET
export TOKEN="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
curl -s -H "Authorization: Bearer $TOKEN" http://127.0.0.1:8000/api/v1/items/1

Proxied request with API key:

curl -s -H "X-API-Key: dev-service-key-1" http://127.0.0.1:8000/api/v1/items/1

Prometheus metrics:

curl -s http://127.0.0.1:8000/metrics

(Set ENABLE_PROMETHEUS=false to disable the endpoint.)

Docker

Default stack (gateway + sample backend)

docker compose up --build

Compose sets API_KEYS=docker-demo-key by default — use that in X-API-Key or mint JWTs with the configured JWT_SECRET.

Redis-backed rate limiting (Compose override)

Use the optional docker-compose.redis.yml file so a Redis service is started and the gateway receives REDIS_URL=redis://redis:6379/0 (distributed token-bucket state across replicas).

docker compose -f docker-compose.yml -f docker-compose.redis.yml up --build

Redis is exposed on localhost:6379 for debugging.

You can still point at an external Redis instead by not using this file and setting REDIS_URL yourself (for example redis://host.docker.internal:6379/0 when Redis runs on the host).

Prometheus and Grafana (Compose override)

docker-compose.observability.yml adds Prometheus (scrapes the gateway) and Grafana (preconfigured Prometheus datasource).

docker compose -f docker-compose.yml -f docker-compose.observability.yml up --build
Service URL Notes
Prometheus http://localhost:9090 Status → Targets → gateway should be UP
Grafana http://localhost:3000 Login admin / admin (change in prod)
Explore Grafana → Explore Example query: gateway_proxy_responses_total

Combine Redis and observability by passing every compose file:

docker compose -f docker-compose.yml -f docker-compose.redis.yml -f docker-compose.observability.yml up --build

Testing

With the virtual environment active:

pytest -v

Tests cover JWT/API-key authentication, token-bucket behavior, routing helpers, and an end-to-end ASGI stack with a mocked upstream transport.

Security notes

  • Rotate JWT_SECRET and API keys for every environment; never commit real .env files.
  • Terminate TLS in front of the gateway (load balancer / ingress) in production.
  • Treat PUBLIC_PATHS as a security surface — keep it minimal in production (often /health and /metrics only, behind network policy).

License

MIT — see LICENSE if present, or add one for your distribution.

About

Built a production-grade REST API Gateway with JWT authentication, Redis-backed rate limiting (token bucket), and middleware-driven request handling. It enforces access control, logs requests, standardizes errors, and is deployed using Docker Compose with a scalable, distributed design.

Topics

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages