Green Build, Red Screen: The API Contract That Only Lives in Code
A new status value passed the frontend build because the OpenAPI spec never mentioned it. Why the contract has to be explicit in the spec.
TL;DR
The frontend built clean but the UI broke because a new ticket status lived in the handler, not the openapi spec. Since frontend types are generated from the spec, the compiler never saw the missing value. The fix was moving enums and descriptions into the spec and enforcing spec-first as the single source of truth.
The Morning the Build Was Green and the Screen Was Red
The afternoon before, I added one new ticket status value to a backend handler in KotaPortal. The frontend build the next morning passed clean, not a single red error in the terminal. Then I opened the ticket list page and the component fell apart: the status column rendered blank, and one panel threw an error over a value it simply didn't know.
My first guess was wrong. I suspected a stale build cache. Deleted node_modules, cleared every cache, ran it all again. Same result. The compile succeeded and the runtime failed. Two hours of investigation collapsed into one sentence: openapi-typescript only translates what is written in openapi.yaml. The new value existed in the handler but not in the spec. To the compiler, it never existed at all.
That's when I recognized a habit I had considered perfectly reasonable: the API contract lives in backend code, and the spec is extra documentation you update when there's time. Meanwhile the KotaPortal frontend takes its API types from the spec through a generator, not from the handler [4]. As long as the spec and the handlers are allowed to differ, both teams are working from separate contracts, and the bill only arrives once the app is running.
The Spec Is Not an Accessory
The OpenAPI Specification was designed as a language-agnostic interface description: humans and machines can understand what an API offers without reading its source code, extra documentation, or sniffing its traffic [1]. When that description is the source of frontend types, a spec missing one enum value means frontend types missing one enum value. Not "slightly stale docs", but a bug the compiler waves through.
The enum part deserves the spotlight. The enum keyword states the possible values of a parameter or property, every value must adhere to the declared type, and the per-value explanation goes into the description field [2]. Enums shared across endpoints can be defined once under global components and referenced via $ref [2]. Almost every object in the spec also accepts a description field for what the machine-readable fields can't carry [3]. Context like "what this status means and when it applies" used to live in handler comments the frontend never read; the right home is exactly there instead.
The Contract Pass: Bidang, Six Statuses, the Rest
The fix was a single pass: the bidang scoping dimension and the six ticket statuses went into the spec as explicit enums, each value with its own description. The ticket history, dates, and link fields moved up into the same contract, closing out the status-vocabulary swap I wrote about last week in the ENUM migration piece. The structure looks roughly like this:
components:
schemas:
TicketStatus:
type: string
enum: [draft, reviewed, approved, rejected, published, archived]
description: |
Six-stage document lifecycle. A value that exists in handlers
but not here is invisible to every generated frontend type.
TicketScope:
type: string
enum: [finance, hr, asset, ALL]
description: |
Unit scope of a record. ALL is the explicit cross-unit value,
not the absence of a choice.
The example is deliberately generic; the pattern is what matters. After regenerating, the compiler does the sweep: every remaining use of an old status turns red on the spot. The ground rule changed too: spec first, handler second. Reversing that means lying to the compiler, and that bill always lands at runtime, the most expensive moment to notice.
My position here is firm: the spec is the only artifact both sides read. Generated types, documentation, validation, even an outside reviewer, all of it is downstream of that one file. If the source is asymmetric, everything derived from it inherits the asymmetry.
What I'll Be Watching
Two old habits could rot this again. First, experimental endpoints: the rush wins, and the handler gets written without touching the spec, justified as "I'll fix it later". Second, values left undocumented because I'm still unsure they belong in the contract. Both are the same trap: the compiler cannot warn about something absent from the contract. The proof is simple enough for anyone to run: delete one enum member from the spec, regenerate, and watch the compiler point at every line still using it. That's real evidence the contract lives in the spec, not in someone's head.
## Sources [1] OpenAPI Specification v3.2.1 — What is the OpenAPI Specification [2] Swagger, OpenAPI 3.0 guide: Enums [3] OpenAPI Learn: Providing Documentation and Examples [4] openapi-typescript, README (repo openapi-ts/openapi-typescript)