API First in Practice: How We Made Frontend Types Predictable and Stable
Source: Dev.to
The Problem: Frontend–Backend Desynchronization
Before adopting API‑First, our workflow looked familiar:
- Backend changes an endpoint.
- Frontend still uses old assumptions.
- Manual TypeScript types become outdated.
- 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.
- Fetch the latest Swagger schema (e.g., from a shared artifact store).
- Regenerate TypeScript types on every pull‑request build.
- Run
tscand 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:
- Start with one service
- Generate types first, requests later if needed
- Enforce usage of generated types
- Add type checks to CI
- 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.