OpenAPI Contract Drift Gate
Context
In May 2026, Lumie stopped treating OpenAPI as documentation-only output and started treating it as a build contract shared between the backend monolith and the Next.js frontend.
The rollout came in two layers:
- backend
f20fa4cdandf7178524: make the published spec codegen-accurate, then freeze it behindOpenApiSnapshotTestand the committedapp/src/test/resources/openapi/api-docs.json; - frontend
51614d3and90c2265: add Orval, generate entity-scoped clients fromopenapi.json, and replace the hand-written query wrappers that had been drifting from controller reality.
Later commits such as 23e3cb67, 84312d2d, f28469be, 3c9988f1, bbab0360, and 4497db03 are not new architecture. They are the intended maintenance shape: contract changes update the snapshot explicitly instead of slipping through silently.
What Broke Or Changed
The original problem was not one bug. It was a mismatch in how the two repos treated the same API:
- springdoc's raw output was not accurate enough for Orval in a few key places, especially pageable query params and response-field requiredness;
- the backend had no committed producer-side golden file, so spec drift could hide inside unrelated controller or DTO changes;
- the frontend had hand-written query wrappers that were expensive to audit across many entities.
The backend fixed the published shape before adding the gate. OpenApiPageableFlattenCustomizer flattens Pageable, and OpenApiResponseRequiredCustomizer marks response fields as present-key required so generated TypeScript matches Java record responses.
Diagnosis And Decision Ladder
- First make the spec trustworthy. A snapshot gate on an inaccurate spec only freezes the wrong contract.
- Then add a producer-side test that fetches live
/v3/api-docs, normalizes it, and compares it to a committed snapshot. - Keep the frontend contract handoff explicit instead of hidden inside CI magic. The backend owns
snapshotOpenApi; the frontend ownsnpx orval --config orval.config.ts. - Route every generated request through one shared mutator so auth cookies, tenant headers, uploads, downloads, and retry behavior stay centralized instead of being reimplemented in generated code.
The producer gate is intentionally simple:
// lumie-backend/app/src/test/java/com/lumie/app/OpenApiSnapshotTest.java
final String raw = mockMvc.perform(get("/v3/api-docs"))
.andExpect(status().isOk())
.andReturn().getResponse().getContentAsString(StandardCharsets.UTF_8);
final String current = normalize(raw);
Resolution
The current split gate has four durable pieces:
lumie-backend/app/src/test/java/com/lumie/app/OpenApiSnapshotTest.javacompares live/v3/api-docsoutput against the committed backend golden file.lumie-backend/app/src/test/resources/openapi/api-docs.jsonis the reviewable producer snapshot updated only when a contract change is intentional.lumie-backend/app/build.gradle.ktsdefines./gradlew snapshotOpenApi, which copies the running backend's/v3/api-docsintolumie-frontend/openapi.json.lumie-frontend/orval.config.tsandsrc/shared/api/orval-mutator.tsgenerate and route consumer calls through the shared request stack.b06ecb6was the important follow-up here because uploads neededFormDatato go throughapiUpload(...), not JSON serialization.
The frontend handoff is still explicit in current source comments:
// lumie-frontend/orval.config.ts
// Refresh the snapshot with `./gradlew snapshotOpenApi` (lumie-backend), then
// regenerate with `npx orval --config orval.config.ts`.
Prevention
- Keep the producer and consumer gates separate. The backend should fail when the published contract changes unexpectedly, and the frontend should regenerate only from an intentional captured spec.
- Fix spec-shape issues in backend customizers instead of teaching Orval or handwritten frontend wrappers to compensate.
- Route generated clients through one mutator. Current
orval-mutator.tsis the place that keeps uploads, downloads, tenant propagation, and shared auth behavior consistent. - Treat snapshot-only commits as normal maintenance, not noise. The repeated
TEST(openapi): update ... snapshotcommits are evidence that the gate is working.
For the day-to-day workflow, the current reference docs are Backend Development, Backend Infrastructure, and Frontend State Management.