This episode covers contract-driven development: deriving types from schemas with openapi-typescript, types from Prisma, and runtime validation with Zod. You'll understand why generated types beat handwritten types.

One of the biggest sources of bugs in modern applications is data shape mismatch between the frontend and backend. Handwritten types in two places drift apart over time. Contract-driven development solves this: one source of truth, types derived from it.
The idea is simple. You define the data contract once, then a tool generates TypeScript types from that contract. The backend and frontend use exactly the same types, born from the same schema, so nothing can drift.
Episode 17 covers three popular tools in this pattern: openapi-typescript for REST APIs, Prisma for databases, and Zod for runtime validation. You'll see why generated types are more trustworthy than handwritten ones.
This pattern replaces manual type definitions with a schema:
bunx openapi-typescript schema.json -o types/api.d.tsThe bunx openapi-typescript command reads an OpenAPI specification and produces a type declaration file. Every endpoint, request body, and response becomes a TypeScript type. When the backend changes the schema, the frontend types are refreshed by running the generate command again.
Manual types can go stale, get mistyped, or be written halfway. Generated types always reflect the latest schema. That's why contract-driven development keeps growing in popularity: synchronization doesn't depend on developer discipline, but on an automated process.
The generated output provides types for every endpoint:
import type { components } from "./types/api";
type Pengguna = components["schemas"]["Pengguna"];
async function ambilPengguna(id: number): Promise<Pengguna> {
const respons = await fetch(`/users/${id}`);
const data = await respons.json();
return data as Pengguna;
}The declaration import type { components } pulls types from the generated file. components["schemas"]["Pengguna"] points to the Pengguna schema defined by the backend. The frontend uses a shape guaranteed to match what the backend will send.
Prisma derives types from a database schema:
model Pengguna {
id Int @id @default(autoincrement())
nama String
}From the schema above, Prisma generates fully typed types and database operations. A query with the wrong property shape is rejected directly by the compiler:
const hasil = await prisma.pengguna.findMany({
where: { nama: { contains: "Budi" } },
select: { id: true, nama: true },
});The type of hasil is derived from select, so you can only access id and nama. Prisma makes the database a second source of truth in the application's type ecosystem.
Compiler types disappear at runtime. Zod brings them back with validation:
bun add zodimport { z } from "zod";
const SkemaLogin = z.object({
email: z.string().email(),
password: z.string().min(8),
});
type InputLogin = z.infer<typeof SkemaLogin>;
const dataAman: InputLogin = SkemaLogin.parse(dataDariClient);The SkemaLogin schema validates data at runtime: email must be valid, password at least eight characters. The z.infer operator derives a TypeScript type from the schema automatically. Zod guards the contract on the most dangerous data path: external input.
The most mature pattern combines all the tools above:
Info
The recommended order: Prisma or another schema as the source of the domain model, OpenAPI for the HTTP contract between services, and Zod at every entry point for external data. Types from all three meet in one types folder so a single change spreads consistently.
When a schema changes, the steps are always the same: update the schema, run generate, and let the compiler point to every piece of code that needs adjusting. Compilation becomes a safety net that keeps the frontend and backend in sync.
Episode 17 changes how you see types: no longer easy-to-drift handwriting, but output generated from schemas. Contract-driven development keeps contracts between systems maintained by process, not by memory.
Key takeaways:
openapi-typescript derives types from an OpenAPI specification.z.infer.In the next episode 18 we'll discuss testing, linting, and editor support for TypeScript — enforcing code quality with tools that understand the type system.