API Gateway Request Flow
tektree intermediate 6 min read
ELI5
The gateway is a hotel concierge: every guest hands over their key card at the front desk (Auth), the desk checks how many requests their room tier allows that minute (RateLimit), and only then does the concierge walk them — note in hand — to the right department.
Technical Deep Dive
The gateway lives at services/api-gateway/cmd/server/main.go and binds Gin on port :8080 with a 15 s read/write timeout and a 60 s idle timeout. Middleware is wired in internal/routes/routes.go and applied as a chain on the protected /api/v1 group.
Request Sequence
sequenceDiagram autonumber participant Client participant GW as api-gateway :8080 participant Mid as middleware chain participant Svc as downstream :8081-8085 Client->>GW: HTTP req + Authorization: Bearer <jwt> GW->>Mid: RequestID (X-Request-ID) Mid->>Mid: Auth — jwt.Parse, set user_id + user_tier Mid->>Mid: RateLimit — Redis token bucket per tier alt rejected Mid-->>Client: 401 / 429 else accepted Mid->>Svc: ProxyTo via httputil.NewSingleHostReverseProxy Note over Mid,Svc: injects X-User-ID, X-User-Tier,<br/>X-Forwarded-Host, X-Forwarded-Proto Svc-->>Client: response (streamed back) endRoute Map (selected)
flowchart LR A["/api/v1/auth/login | register | refresh"] -->|no auth| U[user-service :8081] P["/api/v1/users/*path"] -->|auth| U K["/api/v1/areas|questions|insights|resources/*path"] -->|auth| KS[knowledge-service :8082] G["/api/v1/gamification|achievements|leaderboards/*path"] -->|auth| GS[gamification-service :8083] B["/api/v1/subscriptions|billing/*path"] -->|auth| PS[payment-service :8084]/health and /metrics skip the auth middleware. Auth routes (/api/v1/auth/login, /register, /refresh) also skip auth — they are the way to obtain a token in the first place. Everything else inside /api/v1 runs the full chain.
Forwarding Mechanism
internal/handlers/handlers.go uses httputil.NewSingleHostReverseProxy per target service URL (loaded from internal/config/config.go: UserServiceURL, KnowledgeServiceURL, GamificationServiceURL, PaymentServiceURL, RealtimeServiceURL). Before forwarding, the handler enriches the request with X-User-ID (from the JWT sub claim) and X-User-Tier (from the tier claim), plus the standard X-Forwarded-Host and X-Forwarded-Proto. Downstream services trust these headers and do not re-validate the JWT — the gateway is the single auth boundary.
Key Terms
- Middleware chain →
RequestID → Auth → RateLimitapplied to the protected group only. - ProxyTo → handler that returns a Gin handler closing over a
httputil.NewSingleHostReverseProxy. - Service URL config → keyed by service name, sourced from env in
config.Load(). - Trust boundary → JWT validated once at the gateway; identity flows downstream as headers.
Q&A
Q: A request to /api/v1/users/me returns 401 immediately. Where in the chain did it fail?
A: In Auth (middleware.go:52-97). The header was missing, malformed, or jwt.Parse rejected the signature/expiry; the chain short-circuits before RateLimit runs.
Q: A downstream service receives X-User-Tier: "". What does that imply about gateway config?
A: The token’s tier claim was absent. The gateway’s auth middleware defaults missing tier to "free" (middleware.go:99-132 reads context), so an empty header points to the request bypassing the auth middleware (a public route) rather than a missing claim.
Q: Why does the gateway forward identity as headers rather than re-issuing a token? A: Internal hops are not exposed publicly, and trusting gateway-injected headers avoids a second cryptographic verification per service. The trade-off is that any service reachable outside the cluster network would be a direct auth bypass — keep services internal-only.
Examples
To add a team-admin route group: extend routes.Setup to register protected.Any("/teams/*path", handlers.ProxyTo(cfg.UserServiceURL)) (or a new service URL). The middleware chain is inherited automatically; no per-route auth wiring needed.
neighbors on the map
- Airlock JWT Handoff & Session Cookies debugging login loops or session expiry
- Auth-First WebSocket Handshake debugging AUTH_REQUIRED errors on the relay
- NATS Subject Taxonomy wiring a new consumer to the right stream