MZ
Back to Projects

API Gateway

An API Gateway written in Go, built as a chain of middleware rather than one request handler. It reverse-proxies to upstream services and spreads traffic across their replicas round-robin. Redis holds the rate limit counters and the response cache. Authentication takes a JWT or an API key. A circuit breaker isolates failing upstreams, failed requests are retried automatically, and the rest of the chain covers request and response transforms, IP filtering and body validation. The stack runs under Docker Compose, with Prometheus scraping metrics into Grafana dashboards and traces leaving over OTLP through OpenTelemetry. Request logs land in MongoDB, which also holds the route table that an admin API lists, adds to and deletes from while the gateway is running, so an upstream can be repointed without a redeploy. Nine of the twelve middleware carry their own test file and fourteen test files cover the gateway in total, including one that runs two replicas of the same service and fails unless both serve an even share of the requests.

API Gateway Grafana dashboard showing request throughput and upstream latency

Key technical decisions

Trade-offs that shaped the build: what was chosen, and why over the alternative.

One middleware per concern

Auth, rate limiting, caching, circuit breaking, retries, transforms, IP filtering and validation are each their own middleware, not branches inside a single proxy handler. Ordering between them becomes an explicit contract you have to get right. In exchange every layer has its own test file.

Rate limit counters live in Redis

Counters and cached responses sit in Redis rather than the gateway's own memory. That costs a network hop per request. Keeping them in process would avoid the hop and drop a dependency, but it would also make the gateway stateful: run two replicas and each one enforces its own separate share of the limit, against its own separate cache.

A circuit breaker in front of the retries

Failed requests are retried automatically, but the retry sits behind a circuit breaker. Retrying on its own amplifies load on the service that is already in trouble. Once failures cross the threshold the breaker opens and requests fail fast, and it half-opens later to test whether the upstream came back before closing again.

The round-robin claim has a test behind it

Six requests go through two replicas of the same upstream, and the test fails unless both replicas served exactly three. It also rejects any response that came out of the cache, because a cached response never reaches an upstream and would not count toward either replica. CI runs this on pushes and pull requests to main, alongside golangci-lint.

More projects

All projects