In many teams, frontend and backend evolve in parallel — and very often, they drift apart.
I’ve seen this happen multiple times: API changes, frontend assumptions stay the same, and bugs appear only after the release.
In our company, we decided to treat this problem as a process issue, not just a tooling one. The solution we landed on was an API First approach, where the frontend relies strictly on the API contract defined by the backend — nothing more, nothing less.
This article is about how we implemented this approach in practice and what it changed for our frontend development.
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. And 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 API contract, the frontend doesn’t assume it exists.
How It Works in Practice (CRUD User Example)
Let’s take a simple CRUD example: users.
Before the backend implementation is even finished, the backend team provides us with an OpenAPI schema in YAML format describing the future API.
Here is a simplified but real example of such a schema:
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 if the backend implementation is still in progress.
Generating TypeScript Types and API Requests
On the frontend, we use openapi-generator to generate TypeScript code from this schema.
The generator gives us:
- TypeScript types and interfaces
- API request functions
We don’t write axios calls manually anymore.
We don’t guess request shapes.
We use only what was generated.
For example:
-
getUserclearly returns a User -
createUserexpectsCreateUserRequestThis makes the frontend code extremely predictable.
Generated Types as the Foundation of Frontend Code
Once the types are generated, they are used everywhere:
- API layer
- React components
- Forms
- Validation
- UI state
A very practical example is enums.
From the OpenAPI schema, we get a generated enum like this:
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 filters
- drive conditional UI logic
No hardcoded strings.
No duplicated constants.
No silent mismatches.
CI as an Early Warning System
This approach really shines once CI is involved.
Our process looks like this:
- frontend regularly fetches the latest Swagger schema
- TypeScript types are regenerated
- CI runs
tscand frontend unit tests
If the backend changes something unexpectedly:
- generated types change
- TypeScript compilation fails
- unit tests fail
The important part is when this happens.
Not after the release.
Not in production.
But 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.
Top comments (1)
This is a strong, practical example of API First done right: treating OpenAPI as a strict contract and generating types removes ambiguity, enforces alignment, and turns CI into an early warning system. The real win isn’t the tooling—it’s the discipline that makes frontend types predictable, refactors safer, and cross-team changes visible immediately.