API Design & Documentation
🌐 API Design & Documentation
REST Conventions, Typed
You've already seen REST verbs and resource routes (in the Express courses). The TypeScript-specific question is: what type does each route actually promise to return?
Omit<T, K>
Builds an input type from an entity type by removing server-generated fields like id.
Partial<T>
Makes every field optional — exactly what a PATCH body needs versus a full PUT/POST body.
A Consistent Response Envelope
Wrapping every response in the same generic shape means clients can handle success and failure uniformly, and TypeScript can narrow on status:
This is the same discriminated-union pattern from Course 2 — status is the tag that lets the compiler narrow the rest of the shape.
🛡️ Types Vanish at Runtime
A request body is unknown until proven otherwise. Casting it to a type with as is a promise you haven't verified — a runtime validator is what actually keeps that promise:
Trusting a Cast vs Validating at the Boundary
Unsafe Cast
Runtime-Validated
The payoff: CreateUserSchema can infer its own TypeScript type with z.infer<typeof CreateUserSchema> — one schema defines both the runtime check and the compile-time type, so they can never drift apart.
Generating Docs From Your Types
Hand-written Swagger YAML drifts from the code the moment someone changes a field and forgets the docs. Generating the spec from the same schemas you already validate with keeps them honest:
zod-to-openapi: One Source of Truth
Schema-First
zod/io-ts schemas generate both the TS type and the OpenAPI doc — one definition, no drift.
Decorator-Based (tsoa)
Annotate controller classes/methods; a build step reads the decorators and emits the OpenAPI spec.
🔢 Versioning Strategies
APIs change. The question is how to let old clients keep working while new clients get the new shape:
URL Versioning
/v1/users vs /v2/users — simplest to reason about, but duplicates routing.
Header Versioning
Accept: application/vnd.myapi.v2+json — keeps URLs stable, but harder to test in a browser.
At the type level, a discriminated union keeps each version's shape distinct instead of one type slowly accumulating optional fields for every version that ever existed:
💻 Coding Challenges
Challenge 1: Design the Response Envelope
Write an ApiResponse<T> discriminated union and a handleResponse function that narrows correctly on status for both the success and error branches.
Goal: Practice discriminated unions applied to API response shapes.
Challenge 2: Validate a Request Body
Using a zod-style schema (or a hand-rolled validator function), validate an incoming CreateProductInput body and derive its TypeScript type from the same schema.
Goal: Practice keeping runtime validation and compile-time types in sync from one source.
Challenge 3: Version a Type
Create ProductV1 and ProductV2 types with a version discriminant field, then write a function that accepts either and returns a normalized shape.
Goal: Practice discriminated unions for API versioning instead of one type full of optional fields.
as Is Not ValidationCasting req.body as CreateUserInput tells the compiler to trust you — it does nothing at runtime. Anywhere a type crosses a real boundary (an HTTP request, a file read, a third-party API response), validate it with a real check, not a type assertion. The SQL Injection and XSS courses covered the same principle for a reason: never trust data you didn't produce yourself.
🎯 What's Next
Request and response shapes are now well-typed and documented — next up is Configuration Management: typing environment variables, validating config at startup instead of failing deep inside a request handler, and handling secrets safely.