Spec0
All posts
Mock servers 9 min read

A hand-built mock is a second API you have to keep in sync, and you won't

A mock exists to unblock the people waiting on your API. The moment you maintain it by hand it becomes a second source of truth, and the people building against it are building against a fiction.

A frontend team is blocked. The endpoint they need, POST /checkout, is a fortnight from being built, and they cannot wait a fortnight to start. The backend engineer, being helpful, says: mock it for now. Someone writes a mock. Maybe it is a Postman example, maybe a small Express stub, maybe a JSON fixture checked into the repo. It returns a plausible checkout response, and the frontend team starts building.

For two weeks everything is green. The UI renders, the happy path works, the demo looks finished. Then the real endpoint lands, and integration day is a run of small, stupid surprises. The mock returned status: "active"; the API returns an enum, ACTIVE. The mock had total as a number; the API sends a money object with a currency. The mock never returned the pending_review state that the real service produces about a tenth of the time, so the UI has no branch for it. None of this is hard to fix. All of it is discovered at the worst possible moment, together, off the schedule everyone planned around.

The mock did exactly what it was asked to do. It also lied, quietly, for two weeks.

A copy drifts from the thing it copies

The structural problem is that a hand-built mock is a copy, and copies drift.

Your API already has a description: the spec, or at least the code that serves it. A hand-written mock is a second description of the same contract, maintained separately, by a different person, on a different schedule. There are now two sources of truth for one API, and nothing keeps them in agreement. Nothing even knows they are meant to agree. When the endpoint changes, the spec changes with it, or the code does, and the mock sits where it was, frozen at the shape the API had on the day someone wrote it down.

There is no signal for this. No build turns red because a mock no longer matches the API it imitates. The drift is invisible until a human runs the real thing against expectations that were set by the fake one, which is to say until integration, which is to say too late to be cheap.

sequenceDiagram
    participant B as Backend (the real contract)
    participant M as Hand-built mock
    participant F as Frontend
    B->>M: v1 shape, copied by hand
    F->>M: build against v1 for two weeks
    B->>B: contract moves to v2 (enum, money object, new state)
    Note over M: mock still returns v1, and nothing fires
    F->>B: integration day, against the real v2
    Note over F,B: every mismatch surfaces at once

The tax on the person who owns the endpoint

Day to day, the cost is that the mock is a second job you did not sign up for.

You change the endpoint. Now you also have to remember to change the mock, and the fixture, and the example in the collection, and the copy in the other team’s repo you did not know existed. You update the ones you can see and forget the rest, because keeping a hand-made copy in step with a moving original is exactly the kind of task humans are bad at. The forgetting is not negligence. It is the default outcome of manual synchronisation, and it compounds every time the API moves.

And the person on the other end inherits your forgetting. They are building with confidence against a mock that is quietly a version behind, and the first they hear of it is when their code, which worked perfectly all week, fails against the real service. “Works against the mock, breaks against prod” is one of the more demoralising afternoons in this job, because the tools told you that you were done.

The tax on the people betting on parallel work

Step back, and the thing that is actually breaking is parallel development, which is the whole reason to agree a contract before you build against it.

The pitch for mocks is that teams do not have to wait for each other. The frontend builds against a mock while the backend builds the real thing, and they meet in the middle. That only works if the mock and the real thing are the same contract. When the mock drifts, the two teams are not building toward a meeting point. They are accumulating integration debt that stays invisible until it all comes due at once, on integration day, off-schedule and unpredictable because nobody was tracking the gap.

It is worse than a plain delay, because it launders false confidence. QA passes against a mock that does not match production and reports green. A demo works because it only exercises the happy path the mock happens to get right. Everyone believes they are further along than they are, and the correction arrives late, which is the most expensive time for a correction to arrive.

What “accurate” has to mean

You do not need a statistic to see where hand-maintenance ends up. Picture a mock covering forty endpoints, each with a handful of response shapes. Every change to the API is, in principle, a change to the mock in as many places. The chance a human keeps all of them in sync across months trends to zero, not through carelessness but because the number of small manual updates is larger than attention allows.

There is a second, quieter failure even when someone does keep the mock alive. A mock that returns 200 OK with a body of roughly the right shape is not testing much. If it does not validate the request, it accepts calls the real API would reject, and the consumer learns a contract looser than the truth. If the response does not conform to the schema, including the strict parts, the enums, the nullable fields, the required properties, then building against it teaches the wrong lesson in a way that feels like progress.

graph LR
    SPEC["Published spec (source of truth)"]
    MOCK["Generated mock"]
    C1[Frontend]
    C2[Test suite]
    C3[Agent]
    SPEC -->|regenerated on every publish| MOCK
    C1 -->|builds against the current contract| MOCK
    C2 -->|runs against the current contract| MOCK
    C3 -->|calls the current contract| MOCK

Why the usual options fall short

There is no shortage of ways to stand up a mock. They differ mostly in how fast they drift.

Postman mock servers are driven by the examples you save in a collection. The collection is itself a hand-maintained copy of the API, so it drifts for the reasons above, and because the model is example-based rather than schema-based, the mock will cheerfully return a response that violates the real schema as long as someone saved it as an example. It mocks what you remembered to record, not what the contract says.

Recorded mocks, the record-and-replay style that captures real traffic and plays it back, are accurate at the moment of capture and frozen from then on. They drift the day the API changes, and they look authoritative because the data was once real.

Hand-rolled stubs, WireMock and its relatives, are maximally flexible and maximally manual. Every response is something you wrote and now own forever.

Prism, from Stoplight, gets the important thing right: it generates the mock from the OpenAPI document, so the mock is derived from the contract rather than copied from it, and it can validate against the schema. That is the correct instinct. Where it stops is that it is a process you run against a spec file you point it at, on your machine or in CI, static to whatever version that file happens to be. It mocks a spec, not your registry, so keeping it current is back to being your problem, and serving one consistent mock to every consumer is not what it is for.

The pattern is the one it always is. The tools are good at standing a mock up. Almost none of them are built to keep it honest as the API moves underneath it.

What would have to be true

Argue it from the properties.

The mock has to be generated from the spec, not written as a copy of it. A copy can drift; a projection of the current contract cannot, because there is nothing separate to fall out of sync. The mock should be the spec, rendered as a running server.

It has to be strict in the direction that teaches the truth. It should validate the request and reject what the real API would reject, and shape the response to the schema, enums, required fields and nullability included, so that building against it is building against the real contract rather than a lenient cartoon of it.

It has to move when the spec moves. If publishing a new version of the API does not update the mock, you are back to manual synchronisation with extra steps. The mock has to be tied to wherever the current contract lives, not to a file someone exported once.

It has to be one shared thing, not a localhost process each consumer runs slightly differently. The point is that everyone builds against the same contract; a mock per laptop reintroduces the divergence it was meant to remove.

And it has to be honest about its own limits.

flowchart TD
    R[Request to the mock] --> V{Valid against the schema?}
    V -->|no| E[Reject, as the real API would]
    V -->|yes| S[Respond with a schema-shaped body]
    S --> N{Does a business rule decide the answer?}
    N -->|no| OK[Faithful: the shape is correct]
    N -->|yes| L[Out of scope: shape, not behaviour]

A mock generated from a schema knows shape, not behaviour. It cannot know that checkout fails when the cart is empty, or that a status moves from pending to active only after a webhook lands. Pretending a schema-accurate mock encodes business rules is a fresh way to build false confidence, and the discipline is to use it for the shape and reach for the real service, or a purpose-built fake, when you need the behaviour.

How we come at it

Full disclosure: this is the spec0 blog, and hosted mocks are one of the things we build, so read this section as interested rather than neutral.

spec0 generates a mock server from the published spec. It validates requests and shapes responses against the schema, so a consumer building against it is building against the current contract rather than a hand-made impression of it, and because the mock is tied to the registry it moves when the spec is republished, instead of waiting for someone to remember. It is one hosted surface, not a process each team runs its own way.

The limit is the one above, and we would rather say it plainly than sell past it: this gives you the shape of the contract, not its behaviour. It will not model your business rules, and for stateful or logic-heavy scenarios you still need the real service or a richer fake. What it removes is the specific, avoidable lie of a mock that has quietly stopped matching the API it stands in for.

The part worth arguing about

A mock is useful precisely because it is a simplification, and every simplification is a small lie by omission. A schema-accurate mock removes the lies about shape. It says nothing about the lies of behaviour, and those are the ones that tend to matter on the hard days.

So the question I am left with is where the line sits. How much behaviour can a mock take on before it becomes a second implementation to maintain, with all the drift that implies, one layer up? Record-and-replay buys realism and loses currency. Schema generation buys currency and loses behaviour. Hand-written stubs buy behaviour and lose their minds within a quarter. If you have found a point on that curve that stays honest as the API changes underneath it, I would like to know where it is, and what it cost you to hold it there.