Typing the API Boundary
AdvancedThe types at the network edge are a claim, not a fact. Validating there — and generating types from the backend rather than hand-writing them — is what keeps a full-stack app honest.
Overview
Every bug that survives a green typecheck tends to live at a boundary: an API returned a shape you did not expect, a query param arrived as a string, a nullable column came back null. TypeScript cannot help here on its own because it never sees the runtime value. Two practices fix it. Validate untrusted input with a schema library so the type and the check come from one definition. And generate the client types from the server's OpenAPI schema so the two cannot drift — a hand-maintained copy of your backend's types is a copy that will be wrong.
Validate, Do Not Assert
One schema, giving both a runtime check and a static type.
import { z } from 'zod'
const ProblemSchema = z.object({
slug: z.string(),
title: z.string(),
difficulty: z.enum(['easy', 'medium', 'hard']),
tags: z.array(z.string()),
hints: z.array(z.string()).nullable(),
})
type Problem = z.infer<typeof ProblemSchema> // the type, derived
export async function getProblem(slug: string): Promise<Problem> {
const res = await fetch(`/api/problems/${slug}`)
if (!res.ok) throw new ApiError(res.status, await res.text())
return ProblemSchema.parse(await res.json()) // throws on mismatch
}
// Compare: 'as Problem' claims the shape and fails 200 lines later
// with "cannot read property of undefined", far from the real cause.
// safeParse when a failure is expected and handled
const result = ProblemSchema.safeParse(input)
if (!result.success) return { error: result.error.flatten() }Generated Clients
FastAPI publishes OpenAPI. Generate from it, commit the output, and regenerate in CI.
# FastAPI serves the schema at /openapi.json for free
npx openapi-typescript http://localhost:8000/openapi.json -o src/generated/api.ts
// Then the client types come from the server, not from memory
import type { paths } from '@/generated/api'
type Problem = paths['/problems/{slug}']['get']['responses']['200']['content']['application/json']
// The failure mode this prevents: the backend renames a field,
// the frontend keeps compiling against a stale hand-written type,
// and the bug appears in production as an undefined.
// Make it a CI step, not a habit:
# npm run generate:api && git diff --exit-code src/generated/
// A red build is how you find out the contract moved.Errors and Nullability
Typed errors and honest optionality at the edge.
// fetch does not reject on 4xx/5xx — model the failure explicitly
class ApiError extends Error {
constructor(readonly status: number, readonly body: unknown) {
super(`API ${status}`)
this.name = 'ApiError'
}
}
// Or return a Result instead of throwing, when the caller must handle it
type Result<T> = { ok: true; data: T } | { ok: false; error: ApiError }
// Be honest about nullability. A nullable column is T | null,
// and pretending otherwise moves the crash, it does not remove it.
interface Attempt {
score: number | null // not yet graded
submittedAt: string // ISO string over the wire, NOT a Date
}
// Dates do not survive JSON. Type the wire shape as string and
// convert at the boundary, or every consumer will guess differently.Key Points to Remember
- 1`as` at the network boundary is an unverified claim — validate with a schema so the type and the check share one definition
- 2z.infer derives the static type from the runtime schema, so they can never disagree
- 3Generate client types from the server OpenAPI schema and check for drift in CI rather than hand-maintaining them
- 4fetch does not reject on 4xx/5xx — model API failures with a typed error or a Result union
- 5Dates arrive as ISO strings over JSON; type the wire shape honestly and convert once at the boundary
Interview Questions
Sign in to ask AriaWhy is `await res.json() as User` unsafe, and what should you do instead?
How do you stop frontend types drifting from the backend contract?
How should a Date field be typed on an API response type?
Ask Aria about Typing the API Boundary
Your personal AI tutor — ask anything about this concept
Revision Status
Personal Notes
Sign in to save personal notes for this topic.
Discussion
Sign in to join the discussion.