Integration

Rules on generated types

openapi-typescript already turned the spec into TypeScript. The shape is decided, it is checked in, and it is not yours to move. What is missing is the part OpenAPI cannot express or your server does not enforce — and adding it should not mean writing the shape a second time.

1. The file the generator wrote

Nobody edits this. It is overwritten every time the spec changes.

api.generated.ts
// api.generated.ts — written by openapi-typescript. Do not edit.
export interface components {
  schemas: {
    Order: {
      /** Format: uuid */
      id: string;
      customer: {
        name: string;
        email: string;
      };
      items: {
        sku: string;
        quantity: number;
      }[];
    };
  };
}

2. The rules, declared against it

No second definition of the shape. The paths are checked against the generated type, so a typo in "customer.emial" is a compile error rather than a rule that never fires.

validate-order.ts
import { Builder } from "@maroonedog/luq";
import { requiredPlugin } from "@maroonedog/luq/plugins/required";
import { stringMinPlugin } from "@maroonedog/luq/plugins/stringMin";
import { stringEmailPlugin } from "@maroonedog/luq/plugins/stringEmail";
import { uuidPlugin } from "@maroonedog/luq/plugins/uuid";
import { numberMinPlugin } from "@maroonedog/luq/plugins/numberMin";

interface components {
  schemas: {
    Order: {
      id: string;
      customer: { name: string; email: string };
      items: { sku: string; quantity: number }[];
    };
  };
}

type Order = components["schemas"]["Order"];

export const validateOrder = Builder()
  .use(requiredPlugin)
  .use(stringMinPlugin)
  .use(stringEmailPlugin)
  .use(uuidPlugin)
  .use(numberMinPlugin)
  .for<Order>()
  .v("id", (b) => b.string.required().uuid())
  .v("customer.name", (b) => b.string.required().min(2))
  .v("customer.email", (b) => b.string.required().email())
  .v("items[*].sku", (b) => b.string.required().min(3))
  .v("items[*].quantity", (b) => b.number.required().min(1))
  .build();

3. What happens when the spec changes

This is the part that pays for the direction. Someone upstream renames customer to buyer. You regenerate. The rule that referenced the old field stops compiling:

after-the-spec-changed.ts
import { Builder } from "@maroonedog/luq";
import { requiredPlugin } from "@maroonedog/luq/plugins/required";
import { stringEmailPlugin } from "@maroonedog/luq/plugins/stringEmail";

// The spec renamed `customer` to `buyer`, so the regenerated file says:
interface components {
  schemas: {
    Order: {
      id: string;
      buyer: { name: string; email: string };
    };
  };
}

type Order = components["schemas"]["Order"];

Builder()
  .use(requiredPlugin)
  .use(stringEmailPlugin)
  .for<Order>()
  // The rule was written against the old shape. It no longer type-checks.
  .v("customer.email", (b) => b.string.required().email())
  .build();

The example above is marked must-fail in this page's source. npm run check:doc-examples compiles it on every build and fails if it ever succeeds.

So the claim on this page is not prose. If the drift detection ever breaks, this page breaks CI instead of quietly becoming untrue.

A hand-written schema does not do this. It keeps compiling, keeps validating a field the API no longer sends, and stays green until something fails in production.

After a spec bump
# The spec moves upstream, so regenerate and let the compiler answer.
npx openapi-typescript ./openapi.yaml -o ./src/api.generated.ts
npx tsc --noEmit

Where the same argument applies

Nothing here is specific to OpenAPI. It holds wherever the type is generated and the generator owns it:

Honest limits