Skip to main content

/writing

Building an OpenAPI Code Generation Pipeline with Kubb

· 4 min read

The dashboard I worked on consumed several APIs, all with OpenAPI specs, all changing often. Keeping hand-written TypeScript types in step was not sustainable, so I built a pipeline to generate them.

This is one piece of the dashboard’s integration layer. The pipeline keeps generated code in step with upstream specs, and it sits downstream of the contract-first mocks and the decision to bet on OpenAPI in the first place.

Why Kubb

OpenAPI codegen tools differ on two things that matter. What they generate: types only, types plus runtime validators, or a whole SDK. And how you customize the output: not at all, through templates, or through plugins.

A types-only generator produces clean output but leaves runtime validation as a separate thing to maintain, which is the same synchronization problem the codegen was meant to solve. A full-SDK generator covers everything but tends to emit code that does not read like code a person wrote, and engineers route around code they cannot follow the moment they hit an edge case.

Kubb sits between the two. It generates TypeScript types and Zod schemas from the same spec in one pass, so the validator and the type always come from the same source. The plugin system mattered just as much: I could shape the output to match our naming conventions, so the generated client read like something handwritten. When generated code feels native, nobody wants to fork it.

Preprocessing the specs

The specs needed cleaning before generation. Upstream services emitted documents that were valid but awkward: naming conventions varied between teams, optional and required fields drifted, and response envelopes wrapped payloads differently from one service to the next. None of it was wrong. It is just what happens when each team owns its own spec and the consumers converge them later.

So a preprocessing step ran before Kubb and normalized the input every time. The output stayed stable regardless of upstream wording, which mattered downstream: snapshot diffs then only flagged real contract changes instead of cosmetic ones that flipped with every release.

The automated pipeline

A CI workflow runs on a schedule. It fetches the latest specs, normalizes them, runs Kubb, type-checks the result, and diffs it against the committed snapshot.

The diff is where the pipeline earns its keep. Three things can show up. Added endpoints are mostly safe, though worth a look in case the addition signals a deprecation somewhere else. Removed endpoints break consumers and block the merge until they are updated. Changed shapes are the dangerous ones: same name, different fields, the failure that used to bite us at runtime.

When the diff finds something, the workflow posts a summary to a shared channel naming the endpoints, classifying each change, and linking the spec revision. Engineers see it at the boundary where it happened, rather than when a build starts failing for no obvious reason.

What it regenerates

The pipeline now owns thousands of generated schemas and types across several upstream specs. Nobody edits any of it. When a spec moves, the diff shows up in a channel before a consumer notices, and the regeneration is a pull request somebody reviews rather than a migration somebody schedules.

What I learned

What the pipeline really changed was where breaking changes got noticed. Before, a removed endpoint surfaced as a failing build in whichever consumer touched it first, days after the spec moved, with the cause several steps away from the symptom. Now it surfaces in the scheduled diff, named and classified, before anyone writes code against the old shape.

Codegen only pays back as fast as you regenerate. If the spec changes weekly and regeneration runs monthly, you have bought a delayed integration bug rather than prevented one. The scheduled runs are what made the bet worth taking.

The deeper lesson sits above the tooling. Choosing a tool is downstream of choosing a source of truth. Once the spec is the source, any tool that respects it will do, and the choice comes down to how the output feels to work with. Kubb fit this dashboard. Another project might land somewhere else.