Skip to main content

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 f20fa4cd and f7178524: make the published spec codegen-accurate, then freeze it behind OpenApiSnapshotTest and the committed app/src/test/resources/openapi/api-docs.json;
  • frontend 51614d3 and 90c2265: add Orval, generate entity-scoped clients from openapi.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

  1. First make the spec trustworthy. A snapshot gate on an inaccurate spec only freezes the wrong contract.
  2. Then add a producer-side test that fetches live /v3/api-docs, normalizes it, and compares it to a committed snapshot.
  3. Keep the frontend contract handoff explicit instead of hidden inside CI magic. The backend owns snapshotOpenApi; the frontend owns npx orval --config orval.config.ts.
  4. 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.java compares live /v3/api-docs output against the committed backend golden file.
  • lumie-backend/app/src/test/resources/openapi/api-docs.json is the reviewable producer snapshot updated only when a contract change is intentional.
  • lumie-backend/app/build.gradle.kts defines ./gradlew snapshotOpenApi, which copies the running backend's /v3/api-docs into lumie-frontend/openapi.json.
  • lumie-frontend/orval.config.ts and src/shared/api/orval-mutator.ts generate and route consumer calls through the shared request stack. b06ecb6 was the important follow-up here because uploads needed FormData to go through apiUpload(...), 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.ts is 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 ... snapshot commits 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.