Contract-First Frontend: Stop Letting Your Mocks Lie to You
Frontend teams mock APIs. It’s how we build UIs before backends exist. The problem? Those mocks lie.
You write a mock that returns { "users": [...] }. Backend ships { "data": [...] }. Integration day arrives. Everything breaks. You spend a day debugging what should have been caught months ago.
This isn’t a discipline problem. It’s a tooling problem. Your mocks need to be provably correct—validated against the same contract your backend implements.
+-----------------------------------------------------------+
| THE PROBLEM |
| |
| Frontend Mock -----------------> Looks correct |
| | | |
| v v |
| Backend API -------------------> Implements differently |
| | | |
| v v |
| Integration -------------------> Everything breaks |
+-----------------------------------------------------------+
+-----------------------------------------------------------+
| THE SOLUTION |
| |
| OpenAPI Spec (source of truth) |
| | |
| v |
| Specmatic validates --> Contract matches? --> Serve |
| | | |
| v v |
| Contract mismatch? ----------> FAIL (won't start) |
+-----------------------------------------------------------+
The fix: make mocks that fail when they don’t match the spec. Specmatic does exactly this.
The Core Idea
Specmatic validates your mock responses against an OpenAPI spec before serving them. If they don’t match, the mock server refuses to start.
Your mocks become provably correct.
Why Mocks Drift
Every frontend project starts the same way:
const mockUsers = [
{ id: 1, name: "Alice", status: "active" },
{ id: 2, name: "Bob", status: "inactive" },
];
Then someone adds a field. Or removes one. Or changes status from a string to an enum. The mock keeps working. Tests pass. Demo looks great.
Integration day: nothing works.
This isn’t a people problem. The mock has no connection to reality. It’s fiction.
What We Chose (and Why)
Three options:
| Tool | What it does | Problem |
|---|---|---|
| MSW | Intercepts fetch, serves hand-crafted mocks | Still drifts—no spec validation |
| Prism | Generates responses from OpenAPI | Random data, non-deterministic tests |
| Specmatic | Validates examples against spec, then serves | Exactly what we needed |
Specmatic inverts the model. Instead of “generate something plausible,” it’s “prove this is correct, then serve it.”
The Setup
mocks/
├── specs/ # OpenAPI spec (truth)
│ └── users-api.yaml
├── contract/ # Examples (must match spec)
│ └── users/
│ ├── query-default.json
│ └── query-error-404.json
The spec defines what’s valid. The contracts are examples. Specmatic checks that examples conform to the spec.
A Contract Example
{
"http-request": {
"method": "POST",
"path": "/users/query",
"body": { "pagination": { "page": 1, "pageSize": 25 } }
},
"http-response": {
"status": 200,
"body": {
"data": [
{ "id": "user-001", "name": "Alice", "status": "active" },
{ "id": "user-002", "name": "Bob", "status": "inactive" }
],
"pagination": { "page": 1, "pageSize": 25, "total": 2 }
}
}
}
This isn’t just mock data. It’s a contract that Specmatic validates against your OpenAPI spec.
The Scripts
{
"scripts": {
"test:mocks": "specmatic test --spec-file mocks/specs/users-api.yaml --examples-dir mocks/contract/users/",
"dev:mock": "specmatic stub --spec-file mocks/specs/users-api.yaml --data mocks/contract/ --port 9002",
"predev:mock": "npm run test:mocks"
}
}
The predev:mock hook is the key. You can’t start the mock server if contracts don’t match. The constraint prevents drift.
What Failure Looks Like
$ npm run test:mocks
FAILED: mocks/contract/users/query-default.json
Response body mismatch:
- Missing required field: pagination.total
- Field 'status' has value 'active' but expected enum: [ACTIVE, INACTIVE, PENDING]
1 of 1 contracts failed.
This is good. The mock won’t serve bad data. It refuses to start.
Adding a New Endpoint
Four steps:
1. Add to spec
/users/{userId}:
get:
operationId: getUserById
responses:
"200":
content:
application/json:
schema:
$ref: "#/components/schemas/User"
2. Create contract
{
"http-request": { "method": "GET", "path": "/users/user-001" },
"http-response": {
"status": 200,
"body": { "id": "user-001", "name": "Alice", "status": "active" }
}
}
3. Validate
$ npm run test:mocks
✓ 3 of 3 contracts passed.
4. Develop
$ npm run dev:mock
Specmatic stub server running on port 9002
The response is guaranteed to match the spec.
What Specmatic Doesn’t Do
It’s not a dynamic mock server. Static responses only.
| Want | Reality | Solution |
|---|---|---|
| Filter by status | Same response | Client-side filter |
| Sort by name | Same response | Client-side sort |
| Dynamic pagination | Same response | Client-side slice |
This sounds limiting. It’s actually freeing.
The contract proves shape. The backend implements logic. Keep them separate.
const { data } = await response.json();
// Client-side filtering works fine in dev
const filtered = data.filter(user =>
selectedStatus ? user.status === selectedStatus : true
);
One Contract Per Shape
Don’t do this:
query-status-active.json
query-status-inactive.json
query-status-active-sorted-name.json
Do this:
query-default.json # Happy path
query-empty.json # Empty results
query-error-400.json # Validation error
query-error-500.json # Server error
Specmatic proves shape, not data variety.
When to Skip This
- Prototyping: Still figuring out the API shape? Use MSW.
- Third-party APIs: Can’t write the spec? Use recorded responses.
- WebSockets/GraphQL: Specmatic is REST-focused.
The pattern works when you have an OpenAPI spec and want to enforce it.
Results
Six months in:
- Integration issues dropped ~80%
- New devs productive in hours (just run
npm run dev:mock) - Spec is always current (it’s used daily)
- Contract changes get code review
The counterintuitive part: constraints made us faster. No more integration debugging.
Next
This covered frontend. Next: BFF Pattern with Spring Boot and Specmatic—same contract, validated from the backend.
Your mocks should break before your integration does.