Most teams build APIs backwards. They write code first, then document it later (if at all). By the time frontend developers see the API, it's already built, and changing it is painful.
API-first development flips this around. You design the API contract first, get everyone to agree on it, then build the implementation. It sounds simple, but it changes everything about how teams work together.
What Is API-First Development?
API-first means treating your API specification as the source of truth. You write an OpenAPI document before writing any code, and that document drives everything else:
- Frontend developers build against mock servers
- Backend developers implement the spec
- QA tests verify the implementation matches the spec
- Documentation is generated automatically
Here's the traditional workflow:
Backend code → API → Documentation → Frontend integration → Problems discovered → Rework
Here's the API-first workflow:
API design → Agreement → Parallel development (frontend + backend) → Integration → Success
Why API-First Works
1. Parallel Development
Without API-first, frontend developers wait for the backend to be ready. With API-first, they work simultaneously using mock servers.
Traditional timeline:
Week 1-2: Backend development
Week 3-4: Frontend development (waiting)
Week 5: Integration (discovering issues)
Week 6: Fixing issues
API-first timeline:
Week 1: API design (whole team)
Week 2-4: Parallel development (frontend + backend)
Week 5: Integration (smooth because contract was agreed)
You save 2-3 weeks and avoid integration surprises.
2. Better Design Decisions
When you design the API before implementing it, you think about the consumer's needs first. You're not constrained by your database schema or existing code structure.
Compare these two approaches:
Code-first thinking:
// "Let's just expose our database model"
GET /api/users/123
{
"user_id": 123,
"first_name": "John",
"last_name": "Doe",
"created_at": "2024-01-15T10:30:00Z",
"updated_at": "2024-03-10T14:22:00Z",
"is_active": 1,
"role_id": 5
}
API-first thinking:
// "What does the frontend actually need?"
GET /api/users/123
{
"id": 123,
"name": "John Doe",
"role": "admin",
"joinedDate": "2024-01-15",
"isActive": true
}
The API-first version is cleaner because you designed it for consumers, not for your database.
3. Clear Communication
An OpenAPI spec is a contract that everyone understands. No more "I thought it would return an array" or "I didn't know that field was optional."
# This is unambiguous
paths:
/pets:
get:
parameters:
- name: limit
in: query
required: false
schema:
type: integer
minimum: 1
maximum: 100
default: 20
responses:
'200':
description: Success
content:
application/json:
schema:
type: object
required:
- data
- pagination
properties:
data:
type: array
items:
$ref: '#/components/schemas/Pet'
Everyone knows exactly what to expect.
The API-First Workflow
Step 1: Design the API
Start with user stories and design the API to support them. Don't think about implementation yet.
User story: "As a pet store customer, I want to browse available pets so I can find one to adopt."
API design:
openapi: 3.1.0
info:
title: PetStore API
version: 1.0.0
paths:
/pets:
get:
summary: List available pets
description: Returns a paginated list of pets available for adoption
parameters:
- name: species
in: query
description: Filter by species (dog, cat, bird, etc.)
schema:
type: string
enum: [dog, cat, bird, fish, reptile]
- name: age
in: query
description: Filter by maximum age in years
schema:
type: integer
minimum: 0
- name: page
in: query
schema:
type: integer
minimum: 1
default: 1
- name: limit
in: query
schema:
type: integer
minimum: 1
maximum: 50
default: 20
responses:
'200':
description: Successful response
content:
application/json:
schema:
type: object
required:
- data
- pagination
properties:
data:
type: array
items:
$ref: '#/components/schemas/Pet'
pagination:
$ref: '#/components/schemas/Pagination'
examples:
success:
value:
data:
- id: 1
name: "Buddy"
species: "dog"
breed: "Golden Retriever"
age: 3
description: "Friendly and energetic"
photos:
- "https://cdn.petstore.com/pets/1/photo1.jpg"
status: "available"
- id: 2
name: "Whiskers"
species: "cat"
age: 2
description: "Calm and affectionate"
photos:
- "https://cdn.petstore.com/pets/2/photo1.jpg"
status: "available"
pagination:
page: 1
limit: 20
total: 47
totalPages: 3
components:
schemas:
Pet:
type: object
required:
- id
- name
- species
- status
properties:
id:
type: integer
format: int64
name:
type: string
species:
type: string
enum: [dog, cat, bird, fish, reptile, other]
breed:
type: string
age:
type: integer
minimum: 0
description:
type: string
photos:
type: array
items:
type: string
format: uri
status:
type: string
enum: [available, pending, adopted]
Pagination:
type: object
required:
- page
- limit
- total
- totalPages
properties:
page:
type: integer
limit:
type: integer
total:
type: integer
totalPages:
type: integer
Step 2: Review and Iterate
Get the whole team together to review the spec. This is where you catch issues before any code is written.
Questions to ask: - Does this API support all the user stories? - Are the field names clear and consistent? - Are we exposing too much or too little data? - Will this API scale as we add features? - Is the error handling comprehensive?
Make changes quickly. It's just a YAML file, not thousands of lines of code.
Step 3: Generate Mock Servers
Once the spec is agreed upon, generate a mock server so frontend developers can start working immediately.
Using Prism:
npm install -g @stoplight/prism-cli
prism mock openapi.yaml
Now you have a working API at http://localhost:4010 that returns example responses:
curl http://localhost:4010/pets?species=dog
{
"data": [
{
"id": 1,
"name": "Buddy",
"species": "dog",
"breed": "Golden Retriever",
"age": 3,
"description": "Friendly and energetic",
"photos": ["https://cdn.petstore.com/pets/1/photo1.jpg"],
"status": "available"
}
],
"pagination": {
"page": 1,
"limit": 20,
"total": 47,
"totalPages": 3
}
}
Using Mockoon:
Mockoon is a GUI tool for creating mock servers. Import your OpenAPI spec and it generates a mock server with a nice interface for customizing responses.
npm install -g @mockoon/cli
mockoon-cli start --data openapi.yaml --port 3000
Using Postman:
Postman can also generate mock servers from OpenAPI specs. Import your spec, create a mock server, and share it with your team.
Step 4: Parallel Development
Now frontend and backend teams work simultaneously.
Frontend developer:
// Configure API client to use mock server during development
const API_BASE_URL = process.env.NODE_ENV === 'development'
? 'http://localhost:4010' // Mock server
: 'https://api.petstore.com'; // Real API
async function fetchPets(filters) {
const params = new URLSearchParams(filters);
const response = await fetch(`${API_BASE_URL}/pets?${params}`);
if (!response.ok) {
throw new Error(`API error: ${response.status}`);
}
return response.json();
}
// This works immediately with the mock server
const pets = await fetchPets({ species: 'dog', limit: 10 });
console.log(pets.data);
Backend developer:
// Implement the spec
const express = require('express');
const app = express();
app.get('/pets', async (req, res) => {
const { species, age, page = 1, limit = 20 } = req.query;
// Build query based on filters
let query = db('pets').where('status', 'available');
if (species) {
query = query.where('species', species);
}
if (age) {
query = query.where('age', '<=', parseInt(age));
}
// Pagination
const offset = (page - 1) * limit;
const [pets, [{ total }]] = await Promise.all([
query.limit(limit).offset(offset),
db('pets').count('* as total')
]);
// Format response to match the spec
res.json({
data: pets.map(pet => ({
id: pet.id,
name: pet.name,
species: pet.species,
breed: pet.breed,
age: pet.age,
description: pet.description,
photos: JSON.parse(pet.photos),
status: pet.status
})),
pagination: {
page: parseInt(page),
limit: parseInt(limit),
total: total,
totalPages: Math.ceil(total / limit)
}
});
});
Both teams are working toward the same contract, so integration is smooth.
Step 5: Validate Implementation
Use contract testing to verify your implementation matches the spec.
Using Dredd:
npm install -g dredd
dredd openapi.yaml http://localhost:3000
Dredd makes requests to your API and verifies the responses match the spec.
Using Schemathesis:
pip install schemathesis
schemathesis run openapi.yaml --url http://localhost:3000
Schemathesis generates test cases automatically and finds edge cases you might have missed.
Using Postman:
Generate a Postman collection from your OpenAPI spec and run it as part of your CI/CD pipeline:
newman run petstore-collection.json --environment production.json
Step 6: Generate Documentation
Your OpenAPI spec is your documentation. Use tools to make it beautiful and interactive.
Redoc:
npx @redocly/cli build-docs openapi.yaml --output docs/index.html
Swagger UI:
docker run -p 8080:8080 \
-e SWAGGER_JSON=/openapi.yaml \
-v $(pwd)/openapi.yaml:/openapi.yaml \
swaggerapi/swagger-ui
Stoplight Elements:
<!DOCTYPE html>
<html>
<head>
<script src="https://unpkg.com/@stoplight/elements/web-components.min.js"></script>
<link rel="stylesheet" href="https://unpkg.com/@stoplight/elements/styles.min.css">
</head>
<body>
<elements-api
apiDescriptionUrl="openapi.yaml"
router="hash"
layout="sidebar"
/>
</body>
</html>
Contract-First Principles
1. The Spec Is the Source of Truth
If the spec says a field is required, it's required. If the implementation doesn't match the spec, the implementation is wrong.
Store your spec in version control and treat it like code:
git add openapi.yaml
git commit -m "Add pagination to /pets endpoint"
git push
2. Breaking Changes Require New Versions
When you need to make a breaking change, create a new API version:
servers:
- url: https://api.petstore.com/v1
description: Version 1 (deprecated)
- url: https://api.petstore.com/v2
description: Version 2 (current)
Non-breaking changes (adding optional fields, new endpoints) can be added to the existing version.
3. Automate Everything
Use CI/CD to enforce the contract:
# .github/workflows/api-contract.yml
name: API Contract Tests
on: [push, pull_request]
jobs:
validate-spec:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Validate OpenAPI spec
run: |
npm install -g @stoplight/spectral-cli
spectral lint openapi.yaml
contract-tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Start API server
run: |
npm install
npm start &
sleep 5
- name: Run contract tests
run: |
npm install -g dredd
dredd openapi.yaml http://localhost:3000
4. Involve Everyone in Design
API design isn't just a backend task. Include: - Frontend developers (they're the primary consumers) - Mobile developers (they have different constraints) - QA engineers (they know edge cases) - Product managers (they understand user needs) - Technical writers (they'll document it)
Real-World Example: Adding a New Feature
Let's walk through adding a "favorite pets" feature using API-first development.
Step 1: Update the spec
paths:
/users/{userId}/favorites:
get:
summary: Get user's favorite pets
parameters:
- name: userId
in: path
required: true
schema:
type: integer
responses:
'200':
description: Success
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/Pet'
post:
summary: Add a pet to favorites
parameters:
- name: userId
in: path
required: true
schema:
type: integer
requestBody:
required: true
content:
application/json:
schema:
type: object
required:
- petId
properties:
petId:
type: integer
responses:
'201':
description: Pet added to favorites
'409':
description: Pet already in favorites
/users/{userId}/favorites/{petId}:
delete:
summary: Remove a pet from favorites
parameters:
- name: userId
in: path
required: true
schema:
type: integer
- name: petId
in: path
required: true
schema:
type: integer
responses:
'204':
description: Pet removed from favorites
'404':
description: Pet not in favorites
Step 2: Review with the team
Frontend: "Can we get the favorite count for each pet in the list endpoint?"
Update the spec:
components:
schemas:
Pet:
properties:
# ... existing properties
favoriteCount:
type: integer
description: Number of users who favorited this pet
readOnly: true
Step 3: Update mock server
prism mock openapi.yaml
Frontend can now start building the favorites UI.
Step 4: Implement backend
app.get('/users/:userId/favorites', async (req, res) => {
const { userId } = req.params;
const favorites = await db('favorites')
.join('pets', 'favorites.pet_id', 'pets.id')
.where('favorites.user_id', userId)
.select('pets.*');
res.json(favorites);
});
app.post('/users/:userId/favorites', async (req, res) => {
const { userId } = req.params;
const { petId } = req.body;
try {
await db('favorites').insert({
user_id: userId,
pet_id: petId,
created_at: new Date()
});
res.status(201).json({ message: 'Pet added to favorites' });
} catch (error) {
if (error.code === 'ER_DUP_ENTRY') {
res.status(409).json({ error: 'Pet already in favorites' });
} else {
throw error;
}
}
});
Step 5: Run contract tests
dredd openapi.yaml http://localhost:3000
All tests pass. Ship it.
Benefits and Challenges
Benefits
- Faster development: Parallel work saves weeks
- Fewer integration issues: Contract is agreed upfront
- Better APIs: Designed for consumers, not databases
- Automatic documentation: Always up to date
- Easier testing: Contract tests catch regressions
- Better collaboration: Everyone speaks the same language
Challenges
- Upfront time investment: Design takes time before coding starts
- Discipline required: Teams must stick to the contract
- Learning curve: OpenAPI has a learning curve
- Spec maintenance: Keeping the spec updated requires discipline
The benefits far outweigh the challenges for most teams.
Getting Started
Start small. Pick one new feature and try API-first:
- Write the OpenAPI spec for that feature
- Review it with your team
- Generate a mock server
- Have frontend and backend work in parallel
- Run contract tests before merging
Once you see the benefits, expand to more features and eventually your entire API.
Wrapping Up
API-first development isn't just about writing specs before code. It's about putting the API consumer first, enabling parallel development, and treating your API as a product.
The key is the contract. When everyone agrees on the contract upfront, development becomes smoother, integration becomes easier, and the final product is better.
Start with a simple OpenAPI spec, generate a mock server, and let your frontend and backend teams work simultaneously. You'll never go back to the old way.