Every request into the platform passes through a managed API gateway before it reaches any of the microservices behind it. The gateway is configured with an OpenAPI spec: every route, every backend address, every auth requirement, declared in one document. On paper, one document. In practice, ours is thousands of lines of JSON — and the single most important rule about it is that no human is allowed to edit it.
This post is about how the gateway spec became a build artifact, and the class of bugs that pushed it there.
One giant file is a merge conflict with extra steps
The naive setup is exactly what you'd guess: the spec sits in the repo, you add a route to a service, you scroll through four thousand lines to find the right place, you paste a path block, you deploy. It works when you have ten routes. It stops working shockingly fast after that.
The failure modes stack up. Two changes near each other in the file conflict constantly. Copy-paste propagates subtle mistakes — a stale backend address, a response schema borrowed from the wrong endpoint. And reviewing a diff inside a giant JSON blob is the kind of task where human attention quietly fails: everything looks like everything else, so reviewers skim, and skimming is how wrong config ships.
So I restructured it. Each service owns a directory of per-route JSON fragments — one small file per path, holding just that route's methods, parameters, and auth. A generator script walks the fragments, validates them, and assembles the full spec. The assembled file is still committed, because the deploy pipeline consumes it, but it is understood to be output. The rule is mechanical: change a route in a service, change that route's fragment, run the generator. If you find yourself editing the assembled spec directly, you are making a mistake by definition — whatever you wrote will be silently overwritten the next time anyone runs the build.
That "by definition" framing matters more than the tooling. It converts a fuzzy judgment call ("is this edit safe?") into a rule that requires no judgment at all.
The bug that made auth config sacred
Here's the failure that turned me into a zealot about generated gateway config.
Our auth model runs entirely at the edge: the gateway validates the caller's identity token and forwards the verified claims to backends in a header. Services trust that header and never re-verify tokens themselves — that's the whole point of doing auth at the edge, and I've written about the model before. But it has a sharp corollary I hadn't fully internalised: the gateway's per-route security definition is load-bearing for routes you'd never think of as "secured."
We had a public endpoint — no login required, anyone can call it — that also optionally personalised its response when a logged-in user called it. The service code checked for the forwarded user header and branched. Simple. Except the route's gateway fragment, being "public," had no security definition at all. And a managed gateway that isn't told to process auth for a route doesn't process auth for that route: valid tokens arrived at the gateway and were simply ignored — never validated, never translated into the forwarded header. The backend saw every request as anonymous. Logged-in users got the anonymous experience, and in a later variant of the same mistake, an endpoint that required the user context returned 401s to users who were, by every observable measure, correctly authenticated.
That class of bug is miserable to debug precisely because every component is behaving correctly. The token is valid. The gateway is doing exactly what its spec says. The service is correctly reporting a missing header. The defect lives in the absence of four lines of JSON — and absences don't show up in logs.
The fix was mechanical: public-but-personalised routes get the security definition with the anonymous path allowed. The durable fix was procedural. Auth configuration became part of the fragment template, checked by the generator, so a route cannot be added without an explicit decision about its security block. Drift between what a service expects and what the gateway forwards is now something the build catches, not something users discover.
Config is code, so compile it
The general principle I took away: any configuration large enough to have internal consistency requirements should be generated, not written. Consistency requirements are things like "every route that reads user context must declare auth," "every backend address must point at a real service," "no two fragments may claim the same path." Humans are bad at maintaining invariants across a four-thousand-line file. Generators are perfect at it — checking invariants is what they are for, and it's the same instinct behind deriving several artifacts from a single declared schema instead of keeping them in sync by hand.
Treating the spec as compiled output bought three concrete things.
Reviews got local. A route change is now a twenty-line fragment diff that a reviewer can actually hold in their head, instead of a needle in a JSON haystack.
Invariants got enforced. The generator validates as it assembles. A malformed fragment fails the build on my machine, not in production behaviour.
The scary file stopped being scary. Nobody resolves merge conflicts in generated output — you regenerate it. An entire category of error-prone manual work disappeared.
The cost was an afternoon of writing a build script, plus the ongoing discipline of the never-edit rule. As trades go, it's one of the best I've made on this project.
The takeaway
If you run a gateway, a load balancer, an infrastructure-as-code stack — anything configured through one large declarative document — ask whether that document has invariants a human is currently maintaining by hand. If it does, split the inputs into fragments people can reason about, write the small script that assembles and validates them, and demote the big file to build output. And if your auth happens at the edge, audit the routes where security config is absent, not just where it's present. The bugs that cost me the most weren't in the lines anyone wrote. They were in the lines nobody did.