Skip to content
routebase
Mock ServerAPI DesignTesting

Your Mock Server Is Too Nice

· The Routebase Team

Two isometric structures facing each other, one solid and complete, the other a hollow shell of the same shape

Everyone learns to drive on an empty lot. Flat asphalt, a few cones, nobody else for two hundred metres. It is the right place to start, because you find the biting point without anyone behind you.

But nothing on that lot teaches you what to do when a van pulls out, or when the rain starts halfway through a manoeuvre, or when the car behind you is already too close. The lot is not wrong. It is just too nice, and the first real junction is not a continuation of it. It is a different game.

Most mock servers are that empty lot. Which is fine on day one and expensive on integration day.

What a friendly mock quietly teaches

Start a frontend against a mock that answers in three milliseconds, always with 200, always with the same tidy object, and you will build a genuinely good application, for a world that does not exist.

You will not build a loading state, because nothing was ever slow enough to need one. You will not build an error state, because nothing ever failed. You will not find out what your list view does with zero results, or with four hundred, because the mock returns three every time. You will not discover that two components fetch the same resource on mount, because at three milliseconds nobody notices six requests. And you will not handle 429 at all, because the thought does not arise.

None of this is carelessness. You cannot write a handler for a condition you have never seen. The mock decided which conditions you would see, and it chose the pleasant ones.

Then the real backend arrives. Every one of those missing states shows up in the same week, usually the week before launch. Each of them is a small change to a component that has since been reused in six places.

Three things a mock has to be able to do

The useful question about a mock is not "does it respond?" It is "does it behave?", and behaviour comes down to three properties that have nothing to do with the shape of the response body.

It has to be slow sometimes. A fixed delay is better than none, but a range is what surfaces the real bugs. Those are the request that finishes after the one that started later, the spinner that flickers because it appears for forty milliseconds, and the component that unmounts before its response arrives. In Routebase a rule carries a delay and, optionally, an upper bound to randomise against, so latency varies the way it varies in production.

It has to fail. Not once, on demand, while you are watching, because that only proves the error path renders. It has to fail occasionally, at a probability you set, so that failure lands while you are working on something else and you find out what your retry logic does to a half-filled form. A mock rule can return 500, 502, 503 or 504, and it can do worse, because it can hold the connection open until it times out, or reset it outright. Those last two are the ones that find the bugs, because a clean 500 is a response and a dead socket is not.

It has to say no. Rate limits are the condition frontends handle worst, because they are invisible in development and unavoidable in production. A mock that enforces a request ceiling over a window turns 429 from a thing you read about into a thing your UI has already survived.

Add a fourth if the API has any notion of sequence, which is state. A mock that accepts a POST and then still answers the following GET as if nothing happened is fine for rendering a page and useless for building a flow. Statefulness is what makes "create it, then list it" testable before the backend exists.

The same eight requests against a friendly mock and against production. On the left every response is fast and green, so no loading state and no error path was ever needed. On the right the same calls vary in latency and two of them fail, and those two are the states the frontend never got round to building.

Where the response body comes from matters more

Behaviour makes a mock realistic. The body is what makes it true, and this is where hand-written mocks quietly go wrong.

A mock maintained by hand in the frontend repository is a second opinion about the API. It starts as a faithful copy of the contract and then drifts, because it is edited by whoever needs a field today, in a repository the backend team does not read. By the time it disagrees with the API, nothing announces the disagreement, and the tests still pass, because they are testing against the same fiction.

A mock derived from the contract cannot drift while it is derived. In Routebase the response body for a draft endpoint is projected from the current draft schema at request time, so when you change the schema, the next request through the mock reflects it. There is no regenerate button in that path because there is nothing to regenerate.

That property has an exact boundary, and it is worth stating plainly rather than selling around. The moment you edit the response body by hand, the rule stops following the schema. It is now yours, and a later schema change will not touch it. That is deliberate, because an override that silently disappears on the next edit upstream is worse than no override. Regenerating the body hands ownership back and the rule follows again. And once a spec version is published, the mock pinned to it is frozen on purpose, because a published version that shifts underneath a consumer is not a version. What you get from that point is not silent synchronisation but detection. The rule is compared against the newer contract and flagged, and pulling it forward is a step you take.

So the honest summary is that the mock is derived while it can be, frozen when it should be, and never quietly disagreeing with the contract without saying so.

What this buys the team

The point of all of this is not fidelity for its own sake. It is that "integration" stops being an event.

When the mock is slow, fails, rate-limits and remembers, the frontend that was built against it has already met the conditions the real backend will present. Integration day becomes a change of base URL and a short list of genuine surprises, rather than a fortnight of discovering states nobody designed.

And the backend team gets something too, which is easy to miss. Every question the frontend would have asked in a meeting got asked against the contract instead, early, while changing the answer was still cheap. That includes what this returns when the list is empty, and what happens on a duplicate submit.

If you want the mechanics of that setup, from how rules come off the spec and what matching looks like to where fixtures fit, our API mocking page covers it.

Back to the empty lot

Nobody argues that learner drivers should start in city traffic. The lot earns its place, because you need somewhere to make the first mistakes cheaply.

What makes the lot useful is that you leave it. What makes leaving it survivable is that the last few sessions were not empty, with a bit of traffic, a bit of rain, and someone impatient behind you. A mock server works the same way. It should be the easy place to start, and it should stop being easy well before the day it stops being a mock.

Ready to ship on it?

Routebase is live. Design your API once — docs, mocks, tests, and monitoring all follow from the same source.

14-day Pro trial — no credit card required.