An orders endpoint with a published OpenAPI spec. PR changed the handler's response shape. Spec was not updated. Client codegen (TypeScript SDK) silently went stale. Mesrai cross-referenced the spec and flagged the drift.
The vulnerable diff
// api/openapi.yml — published spec
paths:
/orders/{id}:
get:
responses:
'200':
schema:
type: object
properties:
id: { type: string }
totalAmount: { type: number } # canonical name
// api/orders.ts — handler PR
ordersRouter.get("/orders/:id", async (req, res) => {
const order = await db.orders.findUnique({ where: { id: req.params.id } });
// BUG: shape diverges from spec ({ totalAmount } documented)
res.json({ id: order.id, totalPaise: order.totalPaise });
});What is wrong
OpenAPI specs (or any schema-first contract — gRPC proto, GraphQL SDL, etc.) are the source of truth for the API contract. Handlers must match. When they drift, clients that generate types from the spec end up with type definitions that don't match the runtime response. The fix is either spec-first (update spec + regenerate handler types + handler conforms) or contract test (CI step that validates handler responses against the spec on every PR). The bug class is silent because TypeScript at the handler doesn't know about the spec.
The attack
Symptom in client code:
// Auto-generated client TypeScript:
const order = await apiClient.orders.get(id);
console.log(order.totalAmount); // undefined at runtime
console.log(order.totalPaise); // not in the generated type — errorWeb client breaks first; mobile clients break on next release; partners with their own SDK consumption break silently for weeks.
Mesrai's review comment
mesraipilot · Bot · reviewed 2 min ago
[mesrai] [code-review] [Logic] [OpenAPI-Drift] [medium]
Handler response field `totalPaise` does not match OpenAPI schema
field `totalAmount`. Drift will break code-gen clients.
Options:
1. (Recommended) Update spec + regenerate types + handler conforms:
- api/openapi.yml: rename field or add alongside
- run codegen: pnpm openapi:generate
- handler returns the spec-conforming shape
2. Convert at the handler boundary:
res.json({ id, totalAmount: order.totalPaise / 100 });
(paise → rupees if that's the canonical unit)
Add a contract test to CI so future drift is caught:
// tests/contract.test.ts
import { spec } from "../api/openapi.yml";
test("GET /orders/:id matches OpenAPI schema", async () => {
const r = await request.get("/orders/abc");
expect(r.body).toMatchSchema(spec.paths["/orders/{id}"].get.responses["200"]);
});The fix
// api/orders.ts — fixed
ordersRouter.get("/orders/:id", async (req, res) => {
const order = await db.orders.findUnique({ where: { id: req.params.id } });
res.json({
id: order.id,
totalAmount: order.totalPaise / 100, // paise → rupees per spec
});
});Handler returns the spec-conforming shape with unit conversion at the boundary. CI contract test now fails the PR if the handler diverges again.
Why human review missed it
Spec drift happens whenever the handler changes faster than the spec — refactors, schema migrations, quick fixes. Without a contract test, drift accumulates silently. Mesrai cross-references the spec file with every handler return shape and flags mismatches.
Related rules + further reading
Mesrai rule pack: logic/openapi-handler-drift — flags handler returns that don't match the documented OpenAPI schema.
Pact / Spring Cloud Contract — contract-test patterns.
Common in teams that adopted OpenAPI but didn't wire CI enforcement.
Takeaway
OpenAPI spec + handler must match. Contract test in CI. Mesrai flags drift at PR time.