# Project Typewriter: How we sync types between FastAPI and React

- URL: https://alasco.tech/2026/08/01/project-typewriter/
- Date: 2026-08-01T00:00:00.000Z
- Authors: Nils Pache
- Description: How we built a CI-guarded pipeline using swagger-typescript-api and FastAPI to automatically sync Pydantic DTOs with React TypeScript types.

Hand-maintaining TypeScript interfaces to match backend API responses invites silent bugs, contract drift, and busywork. With Project Typewriter, we built a CI-guarded pipeline using `swagger-typescript-api` and FastAPI’s OpenAPI generation. It derives TypeScript types from Pydantic DTOs so the frontend stays aligned with the backend.

---

## The Problem: The Cost of Hand-Written Types

As Alasco’s workspace grew, so did friction at the boundary between our primary backend (FastAPI, Pydantic) and our frontend app (React, TypeScript).

For a long time, frontend developers manually defined TypeScript interfaces to mirror backend API payloads. That worked at first, but scaling across feature modules exposed familiar problems:

1. **Contract drift:** A backend engineer renames or retypes a field on a Pydantic DTO. The frontend still compiles against stale types, then fails at runtime.
2. **Duplicated effort:** Engineers spent time writing and updating the same schemas in Python and TypeScript.
3. **Flaky test fixtures:** Mock data drifted from real server responses and gave false confidence in unit tests.

We needed a single source of truth: the backend API specification itself.

---

## The Core Generation Stack

### 1. Schema Extraction

We added a command that inspects FastAPI’s `.openapi()` spec in memory and dumps it to an `openapi.json` file. It is fast, deterministic, and needs no running backend services.

### 2. Tailored TypeScript Generation

A frontend script invokes `swagger-typescript-api` via `generateApi()`, pointed at the local schema file.

To keep the frontend clean, we configured the generator like this:

* **Data contracts only:** Extract data models and enums, and skip full Fetch/Axios client classes.
* **Modular generation:** `modular: true`, with output targeting `api/index.ts`.

---

## Keeping the Gate: CI/CD Enforcement

As an interim step we had no CI check and relied on developers to regenerate types whenever they changed backend DTOs. That fell apart quickly. People forgot, and the next person who ran generation got a pile of unrelated diffs from earlier changes. That made it obvious we needed an automatic check.

We added a `types-check` CI gate on every pull request. If someone updates a Pydantic DTO but forgets to commit the updated `index.ts`, CI fails immediately. Contract drift shows up in review, not later in staging or production.

---

## Unstable Exports from Duplicate Class Names

Once generation was running in CI, we hit a subtler problem: schema names were not unique across the Python codebase.

When OpenAPI (and then `swagger-typescript-api`) saw the same class name defined more than once, it prefixed one of the exports with a path-based qualifier so both could coexist in TypeScript. That prefixing was not stable. Depending on import order, one run would leave one class alone and rename the other; the next run would flip which one got the prefix. The generated types bounced between two shapes even when the backend models had not changed, so the CI types-check failed for no useful reason.

The fix was blunt but effective: we inventoried every duplicated class name and renamed or prefixed them in Python until each exported schema title was unique and deterministic. After that, regenerating types produced the same file every time.

---

## The Incremental Migration Strategy

Migrating a large codebase to generated types all at once is risky. We tackled the refactor domain by domain with tech-debt tickets (`Typewriter: Assets`, `Typewriter: Utilities`, `Typewriter: Reporting`, and so on).

For every domain, we applied the same pattern:

1. **Re-export via aliases:** Replace hand-written types in legacy `types.ts` files with re-exports from `@api`:

```typescript
// Before: local hand-written interface
// export interface AssetUser { id: string; name: string; }

// After: re-exporting the generated type
export { type AssetResponsibilityUserRead as AssetUser } from "@api";
```

2. **Update call sites and fixtures:** Point component props, React Query hooks, and Vitest fixtures at the generated `*Read` schema titles from `@api`.
3. **Align test mocks:** Update response builders (for example `buildAxiosResponse(...)`) so mock data matches real backend shapes.
4. **Cleanup:** Once call sites import from `@api` directly, remove the old `types.ts` re-export files.

---

## Key Takeaways

Treating backend Pydantic models as the single source of truth paid off quickly:

* Frontend developers get updated types and editor autocomplete without maintaining parallel interfaces.
* Changing a Pydantic DTO in Python surfaces TypeScript compile errors in React during local builds.
* Clean type generation pushed the team toward clearer backend DTO modeling.
* Unique schema names matter: duplicate class names make codegen non-deterministic until you rename them.