Definition
Contract-first design is an API development approach where you start by defining the complete API contract - endpoints, request/response schemas, authentication, error handling - in a formal specification (usually OpenAPI) before writing any implementation code. This inverts the traditional code-first approach where you build the API first and document it afterward.
The philosophy behind contract-first is that API design is a communication problem, not a coding problem. By creating the contract first, you force upfront discussions about naming, data models, error handling, and edge cases. Stakeholders (frontend developers, mobile teams, partners) review the contract and provide feedback before implementation begins. This catches design issues early when they’re cheap to fix.
Contract-first enables true parallel development. Backend engineers implement the contract while frontend engineers build against mock servers generated from the same contract. Integration happens faster because both sides are coding to an agreed specification. Tools validate that implementations match the contract, preventing drift between docs and reality.
Example
Microservices Team: A team building a new payment microservice starts with an OpenAPI spec defining all endpoints, schemas, and error codes. Product managers, frontend engineers, and backend engineers collaboratively review the spec in design sessions. They identify issues like missing pagination on list endpoints and unclear error responses. After approval, backend implements the spec while frontend generates TypeScript types from it and builds UI against mock servers.
Public API Launch: Stripe designs new API features as OpenAPI specs first. They share specs with design partners (select customers) who provide feedback on usability, completeness, and edge cases before Stripe writes implementation code. This early feedback prevents costly changes after launch.
Mobile App + Backend: A startup building iOS, Android, and web apps starts each sprint by defining API changes as OpenAPI specs. All three client teams immediately generate SDKs from the spec and start building features using mock servers. Backend implements the spec in parallel. When backend finishes, clients swap mock servers for real endpoints with zero code changes.
Enterprise Integration: A large company exposing internal APIs to partners starts with contract-first. Legal, security, and engineering teams review the OpenAPI spec together, ensuring compliance, proper error handling, and security before implementation. The spec becomes the formal agreement with partners.
Breaking Change Prevention: A team wants to add a required field to an existing endpoint. During spec review (before coding), contract diffing tools flag this as a breaking change. The team pivots to making it optional with a default value, preserving backward compatibility.
Analogy
Architectural Blueprints: You don’t start building a house by pouring concrete randomly, then drawing blueprints afterward to document what you built. Architects create detailed blueprints first, get approval from all stakeholders (client, city inspectors, contractors), then construction begins. Contract-first is the same - design the blueprint (API spec), get buy-in, then implement.
Movie Screenplay: Filmmakers write the screenplay before filming. Actors, directors, producers review it, suggest changes, estimate budget. Only after the screenplay is approved does filming begin. Code-first would be like improvising the entire movie during filming, then writing a screenplay to document what happened.
Legal Contracts: When two companies form a partnership, lawyers draft a contract first, both sides negotiate terms, and only after signing do they start working together. No one builds products first then writes a contract describing what they built. API contract-first follows the same principle - agree on terms before investing in implementation.
Code Example
Step 1: Define Contract (OpenAPI Spec)
# contract/api-spec.yaml - Created BEFORE implementation
openapi: 3.1.0
info:
title: Task Management API
version: 1.0.0
description: API for managing tasks in a project
paths:
/tasks:
get:
summary: List all tasks
parameters:
- name: status
in: query
schema:
type: string
enum: [open, in_progress, completed]
- name: assignee
in: query
schema:
type: string
responses:
'200':
description: List of tasks
content:
application/json:
schema:
type: object
properties:
tasks:
type: array
items:
$ref: '#/components/schemas/Task'
totalCount:
type: integer
post:
summary: Create a new task
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/CreateTaskRequest'
responses:
'201':
description: Task created
content:
application/json:
schema:
$ref: '#/components/schemas/Task'
'400':
description: Invalid request
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
components:
schemas:
Task:
type: object
required: [id, title, status, createdAt]
properties:
id:
type: string
format: uuid
title:
type: string
maxLength: 200
description:
type: string
status:
type: string
enum: [open, in_progress, completed]
assignee:
type: string
format: email
createdAt:
type: string
format: date-time
CreateTaskRequest:
type: object
required: [title]
properties:
title:
type: string
minLength: 1
maxLength: 200
description:
type: string
assignee:
type: string
format: email
Error:
type: object
required: [code, message]
properties:
code:
type: string
message:
type: string
Step 2: Generate Mock Server (runs immediately, before backend exists)
# Using Prism to create mock server from contract
npx @stoplight/prism-cli mock contract/api-spec.yaml
# Mock server runs at http://localhost:4010
# Frontend can now develop against it immediately
Step 3: Frontend Development (parallel with backend)
// Generate TypeScript types from OpenAPI spec
// Using openapi-typescript
import type { paths } from './generated/api-types';
type TaskListResponse = paths['/tasks']['get']['responses']['200']['content']['application/json'];
type CreateTaskRequest = paths['/tasks']['post']['requestBody']['content']['application/json'];
// Use generated types for type-safe API calls
async function fetchTasks(status?: string): Promise<TaskListResponse> {
const response = await fetch(`http://localhost:4010/tasks?status=${status || ''}`);
return response.json(); // TypeScript knows exact shape
}
async function createTask(data: CreateTaskRequest) {
const response = await fetch('http://localhost:4010/tasks', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
});
return response.json();
}
// Using the typed functions
const tasks = await fetchTasks('open'); // TypeScript knows tasks structure
const newTask = await createTask({
title: "Implement backend",
assignee: "[email protected]"
}); // TypeScript validates this object matches schema
Step 4: Backend Implementation (validates against contract)
// Backend implements the contract
const express = require('express');
const { OpenApiValidator } = require('express-openapi-validator');
const app = express();
// Automatically validate requests/responses against contract
app.use(OpenApiValidator.middleware({
apiSpec: './contract/api-spec.yaml',
validateRequests: true,
validateResponses: true
}));
app.get('/tasks', (req, res) => {
const { status, assignee } = req.query;
// Implementation...
const tasks = [
{ id: '123', title: 'Task 1', status: 'open', createdAt: '2024-01-01T10:00:00Z' }
];
// Validator ensures response matches contract schema
res.json({ tasks, totalCount: tasks.length });
});
app.post('/tasks', (req, res) => {
// Validator already checked req.body matches CreateTaskRequest schema
const newTask = {
id: generateUUID(),
...req.body,
status: 'open',
createdAt: new Date().toISOString()
};
// Validator ensures response matches Task schema
res.status(201).json(newTask);
});
// If response doesn't match schema, validator returns 500 and logs error
app.listen(3000);
Diagram
graph TB
subgraph "Contract-First Flow"
START[Define Contract
OpenAPI Spec]
START --> REVIEW[Stakeholder Review
Frontend, Backend, Product]
REVIEW --> ITERATE{Changes
Needed?}
ITERATE -->|Yes| START
ITERATE -->|No| APPROVE[Contract Approved]
APPROVE --> PARALLEL[Parallel Development]
subgraph "Frontend Track"
PARALLEL --> GEN_SDK[Generate SDK
TypeScript Types]
GEN_SDK --> MOCK[Mock Server
From Contract]
MOCK --> FE_DEV[Frontend Development]
end
subgraph "Backend Track"
PARALLEL --> BE_DEV[Backend Implementation]
BE_DEV --> CONTRACT_TEST[Contract Tests
Validate Match]
end
FE_DEV --> INTEGRATION
CONTRACT_TEST --> INTEGRATION[Integration
Swap Mock for Real API]
end
style START fill:#90EE90
style APPROVE fill:#FFD700
style INTEGRATION fill:#87CEEB
Best Practices
- Involve all stakeholders early - Frontend, backend, product, security review the spec together
- Use spec linting tools - Run Spectral or similar to enforce design standards on specs
- Generate everything from contract - SDKs, mock servers, tests, docs all come from the spec
- Version the contract - Use semantic versioning, track breaking vs. non-breaking changes
- Automate contract testing - CI/CD validates implementation matches contract on every commit
- Design for evolution - Use extensible patterns (additionalProperties, optional fields) for future growth
- Review before implementation - Catch design flaws in spec review, not during coding
- Mock server in development - Frontend uses contract-based mocks until backend is ready
Common Mistakes
Spec as afterthought: Creating a “contract-first” spec after implementation is done. This defeats the purpose - you miss the design benefits and parallel development.
Skipping stakeholder review: Backend writes the spec alone, doesn’t involve frontend or product. They discover usability issues only after implementation.
Not enforcing contract: Creating a spec but not validating implementation against it. Contract and code drift over time.
Perfect spec paralysis: Spending weeks perfecting the spec before allowing implementation. Iterate quickly, get to working code faster.
Ignoring generated SDKs: Writing the spec but hand-coding clients anyway. This wastes the main benefit of contract-first.
No contract testing: Trusting implementation matches spec without automated validation. Manual testing misses subtle schema violations.
One-time contract: Creating the spec for v1, then abandoning it for future changes. Contract must evolve with the API.