API First in Practice: How We Made Frontend Types Predictable and Stable

Published: (January 11, 2026 at 04:09 PM EST)
5 min read
Source: Dev.to

Source: Dev.to

The Problem: Frontend–Backend Desynchronization

Before adopting API‑First, our workflow looked familiar:

  1. Backend changes an endpoint.
  2. Frontend still uses old assumptions.
  3. Manual TypeScript types become outdated.
  4. Bugs appear late — sometimes only in production.

Even with good communication and documentation, the reality was simple:

The frontend always found out about breaking changes too late.

TypeScript helps, but only if the types are accurate. Manually maintaining those types doesn’t scale.

Our Solution: API‑First as a Contract, Not Documentation

In our team, API‑First means one simple rule:

The frontend uses only the TypeScript types and interfaces generated from the backend API.

The OpenAPI (Swagger) schema is not just documentation. It is the single source of truth. If something is not described in the contract, the frontend does not assume it exists.

How It Works in Practice (CRUD User Example)

1. Backend provides an OpenAPI schema

Before the backend implementation is finished, the backend team delivers an OpenAPI schema in YAML that describes the future API. Below is a simplified but real example:

openapi: 3.0.3
info:
  title: User Service API
  description: API for managing users
  version: "1.0"

paths:
  /users:
    get:
      tags:
        - users
      summary: Get list of users
      operationId: listUsers
      parameters:
        - name: page
          in: query
          schema:
            type: integer
            default: 1
        - name: limit
          in: query
          schema:
            type: integer
            default: 20
        - name: status
          in: query
          schema:
            type: string
            enum: [active, inactive]
      responses:
        "200":
          description: List of users
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/UserListResponse"

    post:
      tags:
        - users
      summary: Create new user
      operationId: createUser
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreateUserRequest"
      responses:
        "201":
          description: User created
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/User"

  /users/{id}:
    get:
      tags:
        - users
      summary: Get user by ID
      operationId: getUser
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        "200":
          description: User data
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/User"

components:
  schemas:
    User:
      type: object
      properties:
        id:
          type: string
          format: uuid
        name:
          type: string
        email:
          type: string
          format: email
        status:
          type: string
          enum: [active, inactive]

    CreateUserRequest:
      type: object
      required:
        - name
        - email
      properties:
        name:
          type: string
        email:
          type: string
          format: email

    UserListResponse:
      type: object
      properties:
        data:
          type: array
          items:
            $ref: "#/components/schemas/User"
        total:
          type: integer

This YAML is enough for the frontend to start working — even while the backend is still being built.

2. Generate TypeScript types and API request functions

On the frontend we use openapi-generator (or a similar tool) to generate TypeScript code from the schema. The generator produces:

  • TypeScript types and interfaces (User, CreateUserRequest, …)
  • Ready‑to‑use API request functions (listUsers, createUser, getUser)

We no longer write Axios calls manually, nor do we guess request shapes. Everything we import comes directly from the generated code, e.g.:

// Example usage
import { getUser, createUser, User, CreateUserRequest } from './api';

async function loadUser(id: string) {
  const user: User = await getUser({ id });
  // …
}

async function addUser(payload: CreateUserRequest) {
  const newUser: User = await createUser(payload);
  // …
}

Generated Types as the Foundation of Frontend Code

Once the types are generated, they become the single source of truth throughout the frontend:

  • API layer – all HTTP calls use the generated functions.
  • React components – props and state are typed with the generated interfaces.
  • Forms & validation – request payload types drive form schemas.
  • UI state & selectors – enums and literal unions keep the UI in sync with the backend.

Example: Enum Generation

From the OpenAPI schema we get a generated enum for user status:

export const UserStatusEnum = {
  Active: 'active',
  Inactive: 'inactive',
} as const;

export type UserStatusEnum =
  typeof UserStatusEnum[keyof typeof UserStatusEnum];

Now the frontend knows that User.status can only be "active" or "inactive". We use this enum to:

  • Build dropdown options.
  • Create filter lists.
  • Drive conditional UI logic.

No hard‑coded strings, no duplicated constants, no silent mismatches.

CI as an Early Warning System

The real power of this approach shines when it’s part of the CI pipeline.

  1. Fetch the latest Swagger schema (e.g., from a shared artifact store).
  2. Regenerate TypeScript types on every pull‑request build.
  3. Run tsc and the full suite of frontend unit/integration tests.

If the backend changes something unexpectedly:

  • The generated types change.
  • TypeScript compilation fails or tests break.

Because the failure happens before the code is merged, the frontend team gets an immediate signal that the contract has diverged, allowing us to coordinate a fix instantly.

TL;DR

Treat the OpenAPI schema as the single source of truth.
Generate TypeScript types and request functions from it.
Make those generated artifacts the only thing the frontend touches.
Run regeneration in CI to catch contract breaks early.

By enforcing this contract‑first workflow we eliminated the classic “frontend‑backend drift” problem, reduced manual type maintenance, and gave our team confidence that the UI and the API stay in lockstep.

Issues Encountered

  • IPT compilation fails
  • Unit tests fail

The important part is when this happens:

  • Not after the release
  • Not in production
  • During CI

This means backend and frontend developers are aware of the problem immediately. Frontend tests become an additional safety net for backend changes.

What We Gained from This Approach

After adopting API‑First, we noticed clear improvements:

  • Predictable and stable frontend types
  • Faster development
  • Fewer questions between teams
  • Earlier detection of breaking changes
  • Much higher confidence when refactoring

The frontend stopped guessing and started trusting the contract.

How to Introduce This in Another Team

If you want to try this approach, my advice would be:

  1. Start with one service
  2. Generate types first, requests later if needed
  3. Enforce usage of generated types
  4. Add type checks to CI
  5. Treat OpenAPI as a contract, not documentation

This is less about tools and more about discipline.

Final Thoughts

API‑First is not about Swagger files or generators.
It’s about predictability.

For a TypeScript frontend, having a reliable contract changes how you think about data. When the API is the source of truth, the frontend becomes simpler, safer, and more confident.

Back to Blog

Related posts

Read more »