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.
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"]
Request flow (simplified):
- Request context — Assign (or accept)
X-Request-ID, emit structured JSON access logs. - Rate limiting — Per-IP token bucket (in-process or Redis-backed).
- Authentication — Public paths bypass auth; otherwise require
Authorization: Bearer <JWT>orX-API-Keywhen API keys are configured. - Proxy — Longest-prefix route match, forward method, path, query string, headers, and body to the upstream; stream responses when appropriate.
| 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"?}} |
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
- Python 3.11+ (tested on 3.12 / 3.13)
pipandvenv
From the repository root:
python -m venv .venvWindows (PowerShell):
.\.venv\Scripts\Activate.ps1
pip install -r requirements.txtLinux / macOS:
source .venv/bin/activate
pip install -r requirements.txtCopy environment template and adjust secrets:
cp .env.example .envRun 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 9000Start the gateway:
uvicorn app.main:app --host 0.0.0.0 --port 8000 --reloadOpen interactive docs at http://127.0.0.1:8000/docs (listed as a public path by default).
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 |
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:9000A request to /api/users/42 is proxied to http://users:8080/42.
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.
The gateway uses a token bucket per client identity:
- Identity — First IP in
X-Forwarded-Forwhen present, otherwise the direct client IP. - Parameters —
RATE_LIMIT_RPScontrols refill speed;RATE_LIMIT_BURSTcaps the maximum burst. IfRATE_LIMIT_PER_MINUTEis set, the implementation raises the effective RPS floor toper_minute / 60so 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.
Health (no auth):
curl -s http://127.0.0.1:8000/healthProxied 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/1Proxied request with API key:
curl -s -H "X-API-Key: dev-service-key-1" http://127.0.0.1:8000/api/v1/items/1Prometheus metrics:
curl -s http://127.0.0.1:8000/metrics(Set ENABLE_PROMETHEUS=false to disable the endpoint.)
docker compose up --build- Gateway: http://localhost:8000
- Sample upstream: http://localhost:9000
Compose sets API_KEYS=docker-demo-key by default — use that in X-API-Key or mint JWTs with the configured JWT_SECRET.
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 --buildRedis 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).
docker-compose.observability.yml adds Prometheus (scrapes the gateway) and Grafana (preconfigured Prometheus datasource).
- Scrape config:
observability/prometheus.yml— jobgateway, targetgateway:8000, path/metrics. - Grafana provisioning:
observability/grafana/provisioning/datasources/datasources.yml.
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 --buildWith the virtual environment active:
pytest -vTests cover JWT/API-key authentication, token-bucket behavior, routing helpers, and an end-to-end ASGI stack with a mocked upstream transport.
- Rotate
JWT_SECRETand API keys for every environment; never commit real.envfiles. - Terminate TLS in front of the gateway (load balancer / ingress) in production.
- Treat
PUBLIC_PATHSas a security surface — keep it minimal in production (often/healthand/metricsonly, behind network policy).
MIT — see LICENSE if present, or add one for your distribution.