Mesrai
Back to blog
// essayTechnical Deep Dive

OpenAPI Drift: When DTOs Lie About Their Shape

Real PR catch: handler return diverges from OpenAPI spec — client codegen breaks silently. Spec-update + contract-test fix.

Mesrai TeamAugust 16, 20268 min read

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

tsapi/orders.ts
// 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:

textexploit
// 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 — error

Web client breaks first; mobile clients break on next release; partners with their own SDK consumption break silently for weeks.

Mesrai's review comment

textreview-comment.txt
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

tsapi/orders.ts (fixed)
// 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.

// try

See it on your next PR.

Free for individuals. Install in two minutes. Mesrai reviews every commit.