Skip to main content

ADR-023: API Gateway + BFF; REST only at the edge

Context

Five frontends (admin/employer/employee/fi/merchant) call the NestJS API. Edge concerns — authentication, idempotency-key minting (ADR-007), rate limiting, API versioning, tenant routing — are currently scattered across middleware and guards, and the SessionMiddleware.forRoutes(...) registration is a hand-maintained class list that has repeatedly drifted (controllers silently 401). As contexts extract into services (ADR-018), the question of where clients enter and how edge policy is applied needs a single answer.

Decision

There is one explicit edge layer that owns auth + idempotency + rate-limit + API versioning. REST exists only at the edge (frontends ↔ gateway, and future partner-facing public APIs). Internal service-to-service communication is gRPC for synchronous calls and events for asynchronous ones (ADR-020) — never internal REST.

Progressive, per ADR-018:

  • MVP: the edge layer is a clearly-named layer inside the existing NestJS app (global auth that respects @Public, idempotency minting, rate-limit, a /api/v1 version prefix) — not a separate deployable. The hand-maintained middleware list is replaced by global application + a CI guard so coverage can't drift.
  • Post-MVP: extract per-audience BFFs (employer/admin/employee/partner) and/or a standalone gateway when frontend-shaped aggregation or independent edge scaling justifies it. The edge contract is documented so this lift touches no domain code.

Alternatives considered

  • Separate gateway + BFFs from day one — rejected: ops overhead before launch; the edge responsibilities can live as one app layer first.
  • Keep edge concerns scattered + the manual middleware list — rejected: it is the single most-stepped-on drift hazard in the repo (auth coverage gaps ship silently).

Consequences

  • Positive: one place for edge policy; auth coverage can't silently drift (CI-guarded); clean seam to lift a real gateway/BFF out later; internal calls stay typed (gRPC), not untyped REST.
  • Negative / accepted: at MVP the "gateway" is logical, not a separate process — some will read the diagram as more than it is; the BFF aggregation benefit is deferred.
  • Follow-ups: consolidate edge concerns into one module; replace forRoutes(...) with global-minus-@Public + a coverage guard; add the /api/v1 prefix.

Revisit when

  • Frontend-shaped aggregation or independent edge scaling is needed → extract per-audience BFFs / a standalone gateway behind the documented edge contract.