Skip to content

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:

ToolWhat it doesProblem
MSWIntercepts fetch, serves hand-crafted mocksStill drifts—no spec validation
PrismGenerates responses from OpenAPIRandom data, non-deterministic tests
SpecmaticValidates examples against spec, then servesExactly 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.

WantRealitySolution
Filter by statusSame responseClient-side filter
Sort by nameSame responseClient-side sort
Dynamic paginationSame responseClient-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.