Pop quiz: how do you know your frontend and backend agree on what a "user" looks like?
If the answer involves "we keep them in sync manually" — you already know the pain. Someone renames a field on the backend. The frontend doesn't know. Nothing breaks at build time. Everything breaks at 2am when real users see blank screens.
Here's a different answer: you define the shape once, and everything else is generated.
The pipeline
Pydantic Schema (Backend)
↓ FastAPI auto-generates
OpenAPI Specification (JSON)
↓ openapi-typescript
TypeScript Types (Frontend) You write the Pydantic schema. FastAPI turns it into an OpenAPI spec automatically. One npm script turns that spec into TypeScript types. Change a field on the backend? The TypeScript compiler tells you every component that needs updating — before you push, before it reaches production.
Step 1: Define your schema with constraints
class TaskCreate(BaseModel):
project_id: UUID
assignee_id: UUID
due_date: datetime
story_points: int = Field(default=3, ge=1, le=21)
priority: TaskPriority # Enum
description: str = Field(..., min_length=3, max_length=500) Notice the constraints — ge=1, le=21, min_length=3. These become part of your API spec. Frontend validation knows the rules without you writing them twice.
Step 2: Wire it into FastAPI
@router.post("/", response_model=TaskResponse, status_code=201)
async def create_task(data: TaskCreate):
... Step 3: Generate TypeScript types (one command)
npx openapi-typescript http://localhost:8000/openapi.json -o src/types/api.ts Step 4: Use them everywhere
// TypeScript now knows every valid task status
const statusColors: Record<TaskStatus, string> = {
open: "blue",
in_progress: "green",
// Add a new status? Compiler demands you handle it here.
}; Six months of results
| Metric | Result |
|---|---|
| Compile-time errors caught | 847 |
| Runtime type errors in production | 0 |
| Migration speed improvement | 57% faster |
| AI generation accuracy improvement | +30% |
Pitfalls to avoid
- The
anyescape hatch: Ban it with ESLint'sno-explicit-any: "error". Everyanyis a hole in your safety net. - Manual type duplication: The moment you hand-write a TypeScript interface that mirrors a backend model, you've created a drift opportunity. All API types must come from generated types.
- Skipping strict mode: Enable
strict: truein tsconfig.json. Strict mode catches the bugs that permissive mode lets through.
Add it to your CI
# In your CI pipeline:
- Start backend (for OpenAPI spec)
- Run: openapi-typescript → generates types
- Check: git diff --exit-code src/types/ # Fail if types changed but weren't committed
- Run: tsc --noEmit # Full type check No type drift gets past this gate. If the backend changes a schema and the frontend types aren't regenerated, CI catches it before merge.