Every frontend team has the same folder. src/mocks/, fixtures/, stubs/ — a pile of JSON files someone wrote during the last backend delay, lovingly shaped to make the demo look good. They have exactly two properties in common: they do not match the real API, and deleting them feels dangerous.
Day 3 of the the series is about replacing that folder with a mock server generated from the OpenAPI spec we wrote on Day 2. The distinction that matters: a mock should answer the contract, not whatever shape made the component render.
Why component-level fakes fail integration
Stubbing the API client inside the app is fine for unit tests. It is a trap for everything else:
- The fixture returns
user.name, the service returnsuser.full_name. Unit tests pass, integration explodes. - Error branches never exist in the fixtures, so nobody builds the error UI until a 500 arrives in a demo.
- Three teams (web, mobile, integration) each maintain their own version of the same lie.
- Nobody can reproduce a partner bug because the fake server cannot produce the sequence of responses that caused it.
A spec-driven mock runs as a real HTTP process. Clients point at it the same way they point at staging — a base URL in an environment switch — and it answers from the document's paths, methods, status codes, schemas, and examples. When the contract changes, the mock changes with it because it is built from it.
Examples are the mock
The trick that makes this cheap: the mock does not need a separate response database. The examples already in the spec become the responses. An operation documented like this:
/holds/{holdId}:
get:
parameters:
- in: path
name: holdId
required: true
schema: { type: string, format: uuid }
responses:
'200':
description: Current hold state
content:
application/json:
schema: { $ref: '#/components/schemas/Hold' }
examples:
active:
value:
id: "3fa85f64-5711-422a-b3c7-8a9d1e2f0001"
slotId: "3fa85f64-5711-422a-b3c7-8a9d1e2f00aa"
status: held
expiresAt: "2026-10-03T14:15:00Z"
...serves that exact body on the mock. Writing examples is documentation work you were supposed to do anyway. The mock makes the work pay rent immediately instead of waiting for a reader.
For operations without examples, the mock generates a body from the schema — enums get their first value, dates get timestamps, arrays get items. That generated body is intentionally obvious; the goal is a running client, not a convincing dataset.
The workflow in practice
- Start the mock from the workspace. It binds a local port and logs every request, including the ones that do not match a documented operation — those mismatches are early contract bugs.
- Point the app at it. Local, mock, staging, and production become entries in an environment switcher: base URL, auth header source, TLS settings. No code changes between them.
- Build the error UI on purpose. Ask the mock for documented 4xx and 5xx cases (most workspaces let you pin a status code per operation) so the empty state and retry logic ship before the backend can disappoint you in public.
- Reproduce partner bugs deterministically. When a consumer reports a failure sequence, script the mock through it: 201, then 409 on replay, then 429. A bug that can be reproduced on demand gets fixed; a bug described in a ticket gets argued about.
The mobile team on the reservations project started the same afternoon the spec stabilized. They did not need a running database, a feature-flagged environment, or a standup to negotiate test data. They needed the contract, which existed.
Streams need mocks too
One surprise: the confirmation flow is an SSE stream, and teams assume streams cannot be mocked, so they skip testing the client until staging. A spec that documents the event stream (media type text/event-stream with an item schema) lets the mock emit the documented events on a timer. The client's reconnect logic, event parsing, and duplicate-event handling — the parts that always break — get exercised at a desk instead of during a release.
When not to mock
The mock is not staging and should not try to be:
- It does not enforce business rules. It will happily return a 201 for a slot the real service rejects, because the contract describes shapes, not policy.
- It is lousy for performance work. Use the real thing.
- Stateful sequences (create then read the created id) belong in scenario tests — Day 4 — not in static mock responses.
Keeping that boundary clear is what keeps the mock honest. It answers "what does the contract say happens," which is exactly the question frontend development needs answered while the backend is still being built.
The payoff on switch day
When the service is ready, nothing dramatic happens. The environment switches from mock to staging. Requests that matched the contract keep working; requests that only ever matched the old hand-written fixtures start failing immediately, which is the system working as intended — you wanted that gap visible in week two, not in the release.
The pattern is tool-independent: any OpenAPI-aware mock server plus disciplined examples gets you most of it. I run it inside Powerduck because the spec, mock, environments, and tests share one local workspace, and I wrote the longer version of the blocking argument — including why fake objects demo well and integrate badly — on the company blog.
Tomorrow, Day 4: the mock answers correctly, but does the workflow work? Scenario tests that chain real requests, pass data between steps, and treat a 200 as the beginning of the test instead of the end.
Top comments (0)