CRUMB a card from devarno-cloud

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)
end

Route 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 chainRequestID → Auth → RateLimit applied 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