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 — 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.
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:
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.
# 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:
- Prisma and Drizzle, regenerated when the migration lands
- protobuf and GraphQL codegen, shared with services you do not build
- A generator or a model writing the rules — the wrong path is rejected before the code is ever run
Honest limits
-
Luq reads the generated type, not the OpenAPI document. Constraints the
spec carries —
maxLength,pattern,format— do not survive into TypeScript, so you write those rules yourself. If you want the document itself to drive validation, that is the JSON Schema entry point, and it is a different trade. - A renamed field is caught. A field whose type widens in a way your rules still accept is not — that is a change the compiler has no reason to reject.