The Gateway as Policy Boundary
Authentication verification, rate limits, size caps, request IDs, TLS, routing and version steering can live at the gateway — one enforcement point instead of N reimplementations. The discipline is knowing which contract clauses belong at the edge, and remembering that gateway-generated responses are part of your contract too.
Frame the contract
API design starts with a consumer, a design question and a guarantee — never with a URL.
What the edge should own: the uniform clauses
Some contract clauses are identical for every endpoint, and enforcing them per-service means N implementations, N configurations, and N chances to drift. The gateway is where those clauses consolidate: authentication verification (validate the token's signature and expiry once, pass verified identity downstream as trusted headers or context — the mechanics live in the security domain; the gateway is where the contract's "requests must be authenticated" clause physically runs), rate limits and quotas as documented behavior — the 429s, Retry-After and limit headers of The Rate-Limit Contract — since only the edge sees a consumer's traffic across all services, request size caps rejecting oversized bodies with 413 before they reach anyone's memory (see Large Requests and Documented Limits), request-ID minting (see Request IDs: The Contract's Correlation Clause), TLS termination, edge Compression: Cheaper Bytes, Not Fewer, CORS, and uniform access logging and metrics — the one vantage point that sees what consumers actually experience (see API Metrics: Rate, Errors, Duration, Sizes).
The test for edge placement is: *does enforcing this clause require any domain knowledge?* Verifying a signature, counting requests per key, measuring a body in bytes and stamping an ID need none. The moment a decision needs to know what a "project" is or who may modify one, it has left gateway territory.
| Clause | Belongs | Why |
|---|---|---|
| Token verification (authn) | Gateway | Identical per route; zero domain knowledge; one place to patch |
| Resource authorization (authz) | Service | "May this user modify prj_42?" requires domain data the edge must not own — see Authorization Design in the Contract |
| Rate limits / quotas, 429 + Retry-After | Gateway | Only the edge sees the consumer's total traffic; the contract behavior is uniform |
| Request size caps → 413 | Gateway | Byte-counting; protects every service at once |
| Request-ID mint, access log, RED metrics | Gateway | The consumer's-eye vantage point |
| Body validation, domain rules | Service | Requires the schema and the domain; edge copies drift from the source of truth |
| Version routing (URI/header → backend) | Gateway | Steering is mechanical — see Versioning: What a Version Even Promises; the shapes themselves live in services |
| Response shaping / business fallbacks | Service (or BFF) | Business logic in gateway config is logic outside tests, review and type systems — see Backend for Frontend |
Gateway responses are contract responses
From the consumer's side there is no gateway — there is one API. The 429 your rate limiter emits, the 413 from the size cap, the 401 from token verification, and the 502/504 when an upstream dies are all responses *your contract* produced, and consumers will write branching and retry logic against them. The default failure is a gateway speaking its vendor's dialect: an HTML error page or {"message": "Limit Exceeded"} from the gateway while services emit your documented envelope — so every consumer needs two error parsers, and the one for the edge cases is the one they never tested (see The Error Model: Structure Over Apology).
The fix is to configure the gateway to speak the contract: the same error envelope (machine-readable code, human message, request_id), the promised headers (Retry-After and limit state on 429s — see Retryability: Telling Clients What To Do Next), and documentation that owns the edge responses explicitly: every endpoint can also return 401, 413, 429, 502, 504, with these shapes and these retry semantics. Timeout policy needs the same single-owner treatment: the gateway's upstream timeout must exceed any service's intended processing window, or long requests are killed at the edge into 504s while the service finishes work nobody will receive — the misalignment behind many "Retries and Timeouts as Contract Guidance" incidents, and an argument for Long-Running Operations: 202 and the Job Resource never hiding behind a synchronous edge.
POST /projects/42/tasks HTTP/1.1
Host: api.example.com
Authorization: Bearer <token>
{ "title": "Write the Q3 report" }HTTP/1.1 429 Too Many Requests
Retry-After: 12
RateLimit-Limit: 600
RateLimit-Remaining: 0
X-Request-Id: req_01HQX5AW2K
Content-Type: application/json
{
"error": {
"code": "rate_limited",
"message": "Rate limit exceeded. Retry after 12 seconds.",
"request_id": "req_01HQX5AW2K",
"retryable": true
}
}The failure modes: the dumping ground and the drift
The gateway's convenience is its hazard. Because the edge touches every request and deploys as configuration, it attracts logic that is faster to add there than to route through a service team's backlog: a header-based feature flag, a response rewrite for one client, a retry rule for one flaky upstream. Each is one YAML stanza; together they become a shadow service — unversioned, thinly tested, owned by a platform team that does not know the domain, invisible to service-owning teams debugging behavior their code does not produce. The architecture domain covers the pattern at scale (API Gateway); the contract-level rule is simpler: the gateway *enforces* clauses, it does not *invent* behavior. Anything a consumer would call semantics belongs in a service.
The second failure is drift between layers. The gateway advertises /v2/... routes that no longer match the service's actual paths; the size cap is 1MB at the edge and 10MB in service validation (so the documented 10MB limit 413s at 1MB); authn is enforced at the gateway, and an internal caller bypassing the gateway reaches the service unauthenticated — the edge check must be a *first* line, with service-level verification behind it, or your security boundary is a routing rule (Defense in Depth is the principle). Drift prevention is mechanical, not aspirational: derive gateway config from the same OpenAPI: Describing the Contract, Not Designing It source of truth the services build against, test the edge policies in CI like any other contract surface (see Testing the Contract, Not Just the Code), and give every policy an owner of record (see API Ownership and the Catalog).
And plan for the gateway's own operational weight: it is a hop on every request (typically single-digit milliseconds — cheap, not free), a fleet to scale, and a blast radius of *everything* when its config deploy goes wrong. A staged rollout discipline for gateway config equals its blast radius, which is total.
Key points
- Consolidate the uniform clauses at the edge: authn verification, rate limits and quotas, size caps, request IDs, TLS, compression, uniform logging and metrics.
- The placement test is domain knowledge: byte counts and signatures belong at the gateway; "may this user modify this project" never does.
- Consumers see one API — gateway-emitted 401/413/429/502/504 responses must use the documented error envelope, headers and retry semantics.
- Align timeout budgets across layers: an edge timeout shorter than a service's processing window converts slow successes into 504s.
- The gateway enforces clauses; it must not invent behavior — edge config that encodes business logic is a shadow service outside tests and review.
- Prevent drift mechanically: generate edge config from the same spec services build against, test edge policies in CI, keep service-level enforcement as the second line.
Follow the failure
How the contract fails or gets misused, hop by hop — and what it costs when it completes.
- 1Platform team → gateway: centralizes authn, limits and routing; service teams delete their per-service copies and everything gets simpler.
- 2Product pressure → gateway config: a response rewrite for one mobile client and a retry rule for a flaky upstream get added at the edge — faster than waiting on two backlogs.
- 3Gateway → consumers: the vendor-default 429 (HTML body, no Retry-After) starts appearing under load; consumer retry code, written against the documented envelope, misparses it and retries instantly, amplifying the load.
- 4Edge ↔ services: the gateway's 1MB body cap contradicts the documented 10MB limit; uploads between the two sizes fail with an error shape nobody documented.
- 5Internal service → backend: a new internal caller reaches a service directly, bypassing the gateway — and discovers that "authenticated at the edge" meant the service itself checks nothing.
- Consumers hit undocumented response shapes exactly at the worst moments — under rate limiting, oversize requests and upstream failures — where their untested error-handling branches live.
- Behavior nobody's code produces: service teams debug responses that originate in edge config they cannot see, and every cross-team incident starts with "is this us or the gateway?"
- A bad gateway config deploy is a total outage: the blast radius of the single enforcement point is every route at once.
Design, observe, evolve
A contract decision is incomplete until you know how you would notice it failing and how it changes later.
- • Write the placement policy down: an explicit list of which contract clauses the gateway enforces, owned like any API surface (see [[api-ownership]]).
- • Configure custom error responses at the gateway so every edge-emitted status uses the documented envelope, request ID and headers; document edge statuses as part of every endpoint's contract.
- • Generate routes, size caps and version steering from the same schema source the services build from, and diff-test gateway config in CI against the spec.
- • Keep services enforcing authn and authz behind the edge — the gateway is the first line and the optimization, never the only line.
- • Edge-vs-service disagreement metrics: requests the gateway rejected that the service would have accepted (and vice versa) — the direct measurement of policy drift.
- • Gateway-emitted status rates (401, 413, 429, 5xx from the edge) tracked separately from service statuses, so "is this us or the gateway?" is a graph, not an investigation.
- • Config-change correlation: gateway deploys annotated on every consumer-facing dashboard, because the edge's blast radius makes it the first suspect after any global anomaly.
- • New uniform clauses (a new auth scheme, a new limit header) roll out at one point instead of N services — the gateway is where contract-wide evolution gets cheap.
- • Version steering at the edge lets old and new backends coexist during [[api-migration]]: route `/v1` to the frozen service, `/v2` to the new one, and retire routes with telemetry.
- • When edge config accumulates domain behavior, evolve it out deliberately: promote each rewrite or fallback into an owned service (or a [[backend-for-frontend]]) before it calcifies into unowned load-bearing YAML.
- • One more hop on every request and one more fleet to run — a few milliseconds and real operational surface purchased for uniformity.
- • A single enforcement point is a single point of failure and a total blast radius; gateway config needs deploy discipline most teams reserve for their riskiest service.
- • Centralizing policy splits contract ownership between platform and service teams — the seam needs explicit rules, or the two halves of one contract drift apart.