Build1 distinct publisher3 min readUpdated
A hand-written TypeScript copy of a Pydantic schema kept the compiler happy and the data wrong. The durable fix was deleting the copy and generating the contract from the backend.
The Engineer · Build desk

Compiled by The EngineerSomething wrong?How this is made
A developer shipped a build with a clean type check, 263 passing tests, and every nutrient value on the screen reading `undefined` [1]. The suite was green because it was comparing the frontend's hand-written copy of the backend schema against itself, which is the failure mode that reports success in exactly the tone of voice that means ship it [4][6].
The project is Pantensa, a household pantry and nutrition tracker, FastAPI and PostgreSQL behind a Vue frontend [2]. The work in flight was replacing eight hard-coded nutrient columns with a proper many-to-many catalog [3]. The frontend carried a mirror of the backend's read schemas: a TypeScript interface per Pydantic model, kept in sync by the author remembering to keep it in sync [4]. He renamed fields on the backend, the mirror kept describing the old shape, the tests kept asserting against the mirror, and the compiler agreed with both [5]. Internal consistency is not correctness, and a green suite in that configuration carries no information about the wire at all [6].
Two fixes were available and both were worse. Committing a fixture of the API response moves the problem one file over, because the fixture becomes the new unchecked copy and drifts the way the interfaces did [9]. Standing up a real FastAPI process inside Vitest pulls a cross-language runtime into the unit layer, which the author calls the wrong altitude and flaky besides [10].
What he did instead was remove the copy: the Pydantic schema is the single author of the wire, everything downstream is derived from it, and the derivation runs on a machine [11]. `frontend/openapi.json` is committed as a deterministic projection of `app.openapi()` [12]. Three gates hold the chain. A backend test asserts that `app.openapi()` equals the committed file, so renaming a field without regenerating fails the backend suite, on the side that made the change rather than the side that suffers from it [13]. `openapi-typescript` regenerates the TypeScript types from that file before every entry point, git-ignored, rebuilt by a pre-hook on type-check, lint, test, coverage, build and dev, so there is no stale artifact available to drift [14]. The frontend's read types are then derived with `Pick` over the generated schemas, so a rename that survives the first gate deletes a key and `vue-tsc` goes red [15]. The `Required<>` wrapper is load-bearing: OpenAPI marks defaulted fields optional, and stripping the `?` while keeping `| null` leaves the compiler as the guard instead of letting a `?? 0` silence it [16]. Net effect on the catalog path: one committed copy of the wire shape, and a test that asserts it against the generator [22].
One field is exempt on purpose. `nutrients` stays an explicit `Partial<Record<NutrientKey, number>>` rather than deriving from the generated index signature, because the sparse invariant, that a missing fiber value is not a fiber value of zero, is precisely what generation would erase [17]. The stated rule is to derive everything whose shape the backend owns, not everything [18].
The honest accounting is in the author's own cost note: the mechanism is repo-wide, the enforcement is catalog-only [19]. The derived types cover one bounded context; the rest of the frontend still reads the wire the old way and would still go green while lying [19]. He puts the coverage at about a quarter of the surface and says extending it is mechanical and not done [19][20].
Watch whether the remaining contexts actually get converted, since mechanical and undone is the state most gates die in [19]. Watch the exemption list, because every hand-maintained type is a mirror waiting to rot [17]. And watch the import layer, where the same discipline is applied differently: ESLint sorts every file into one of six element types and forbids any dependency it does not explicitly name [21].
Follow any of these and your For You feed starts watching them — no settings page required.
Ranked by verification strength, evidence, and original report placement.
The type check was green, all 263 tests were green, and every nutrient value on the screen read undefined.
The project is Pantensa, a household pantry and nutrition tracker, with FastAPI and PostgreSQL on the backend and Vue on the frontend.
The work was midway through replacing eight hard-coded nutrient columns with a many-to-many catalog.
The frontend carried a hand-written mirror of the backend's read schemas: a TypeScript interface for every Pydantic model, kept in sync by the author remembering to keep it in sync.
The author renamed fields on the backend; the mirror still described the old shape, the tests still asserted against the mirror, and the compiler agreed with both, so everything was internally consistent and none of it was true.
The author's framing: a red suite tells you something, while a suite that is green because it agrees with itself tells you nothing, and does so in the exact tone of voice that means 'ship it'.
Evidence-backed comparisons of source perspectives and observed adoption signals. Read the methodology
Which Builder, Operator, and Investor concerns the observed source mix emphasized—not a truth score.
Evidence, demonstrated adoption, hype gap, incentives, and confidence are assessed independently, each on its own current evidence. How these are measured.
Detailed but single-source and self-reported
The mechanism is described with unusual specificity — the exact assertion (app.openapi() == frontend/openapi.json), the entry points carrying the regeneration pre-hook, the derived Required<Pick<...>> type, and the reason Required<> and the nutrients exemption matter — which makes the technique reproducible by a reader. But every factual element, including the 263-test count and the undefined-value bug, comes from one first-person post about the author's own private repository, with no independent verification, no linked repo output, and no second publisher in the cluster.
One solo project, one bounded context
The only observed adoption is the author's own use inside a single hobby project, and the author states enforcement reaches just the catalog bounded context — roughly a quarter of the frontend surface — while the rest still reads the wire the old way. No other teams, organizations, downloads, or deployments are disclosed anywhere in the supplied material.
Slightly understated by the author
The narrative headline is dramatic, but the substance is bounded by the author rather than inflated: he names the coverage limit (catalog-only, about a quarter of the surface), keeps a deliberate exemption instead of claiming full derivation, states the rule as deriving only what the backend owns, and rejects two easier fixes on stated technical grounds. Weighed against a thin, single-source evidence base and near-zero external adoption, the framing lands marginally below what the claims could have supported, hence a small negative gap.
Self-authored practitioner account, no disclosed commercial tie
The post is an author writing about his own solo codebase on a developer publishing platform, which carries a mild reputational incentive to present his engineering decisions favorably and no external reviewer to contradict him — he states nobody reviews his pull requests. Against that, the supplied material discloses no vendor relationship, no product being sold, no funding, and no sponsorship, and the tools named (FastAPI, Pydantic, openapi-typescript, ESLint) are open-source with no stated affiliation.
Confident about the described practice, not about its generality
Confidence is high that the article says what is recorded here and that the mechanism as described is internally coherent, because the code and gate structure are quoted directly. Confidence is much lower on outcomes and generality: one publisher, one self-reported project, no external validation, and an author-acknowledged coverage limit mean the reported benefit cannot be checked or extrapolated.
build
Return the admission record, not the log line: one memory service's case for receipts1 distinct publisher
build
The NestJS default path puts the query inside the business rule, and nothing fails when it moves1 distinct publisher
build
A RAG stack lived seven hours before a hosted embedding endpoint returned 4041 distinct publisher
build
A reply bot's confidence score was always 0.85, because it was typed in, not computed1 distinct publisher
Distinct publishers with included, body-backed reporting in this cluster.
dev.to
1 article · August 17, 2026