The dependency that shouldn't block you
"We can't build the dashboard until the backend team ships the /stats endpoint" is a scheduling problem, not a technical one — the frontend only needs to agree on the *shape* of the JSON it'll receive, not wait for a real implementation to exist. This post covers the workflow, not the data generation itself — see Random JSON Generator if what you need is realistic fake *values*; this is about wiring mocked JSON into your actual dev loop so it behaves like the real API will.
Step 1: Agree on the contract before writing UI code
A mock is only useful if it matches what the backend will eventually return — so the first artifact isn't code, it's a JSON sample both sides sign off on:
{
"revenue": 48213.50,
"orders": 341,
"topProducts": [
{ "sku": "SKU-1", "name": "Widget Pro", "unitsSold": 88 }
]
}This can come from an API design doc, an OpenAPI spec's example field, or — fastest in practice — the backend engineer pasting a realistic sample straight from their in-progress work, even before the endpoint is deployed anywhere. JSONKit's Random JSON Generator is useful here too: define the field shapes visually and generate a batch of varied, realistic-looking records to build and test the UI against more than one example, not just the single "happy path" sample from the design doc.
Step 2: Intercept the fetch, don't fork the code
The failure mode to avoid is component code that branches on an environment flag (if (USE_MOCK) return mockData; else return fetch(...)) — that's a permanent maintenance burden and a real chance the mock path silently diverges from the real one. Instead, intercept the *network layer* so component code is identical in both cases.
Mock Service Worker (MSW) intercepts fetch/XMLHttpRequest at the network level, in the browser or in Node for tests:
// npm install msw
import { http, HttpResponse } from "msw";
import { setupWorker } from "msw/browser";
const worker = setupWorker(
http.get("/api/stats", () => {
return HttpResponse.json({
revenue: 48213.50,
orders: 341,
topProducts: [{ sku: "SKU-1", name: "Widget Pro", unitsSold: 88 }],
});
}),
);
if (process.env.NODE_ENV === "development") worker.start();Your component's fetch("/api/stats") call never changes — MSW sits below it, transparently. Flip a flag or an environment variable to disable the worker once the real endpoint exists, and delete the handler once it's no longer needed; nothing in the component tree ever knew the difference.
json-server is the lighter-weight alternative when you want a real, separately-running mock HTTP server rather than an in-browser interceptor — point it at a JSON file and it serves a full REST API (GET/POST/PUT/DELETE, even query filtering) from it in one command:
npm install -g json-server
echo '{ "products": [{ "id": 1, "name": "Widget" }] }' > db.json
json-server --watch db.json
# GET http://localhost:3000/products now works, no backend code writtenStep 3: Model the states the real API will have, not just the happy path
The UI bug that ships most often against a mock is one that only handles the empty-array, error, and loading states in theory, because the mock only ever returned the happy path. Build at least three response variants into the mock from the start:
http.get("/api/stats", () => {
const scenario = new URL(location.href).searchParams.get("mock");
if (scenario === "empty") return HttpResponse.json({ revenue: 0, orders: 0, topProducts: [] });
if (scenario === "error") return HttpResponse.json({ error: "Internal error" }, { status: 500 });
return HttpResponse.json({ revenue: 48213.50, orders: 341, topProducts: [/* ... */] });
});A query-param-driven scenario switch like this lets anyone reviewing the UI — a designer, a PM, QA — pull up the empty and error states directly by URL, without needing dev tools open or a debugger breakpoint.
Step 4: Swap to the real API with a config change, not a rewrite
Because the interception happens below the component layer, "swapping to the real backend" should mean flipping one flag or environment variable, not touching component code. If it means more than that, the mock diverged from the real contract's *shape* — usually a field the mock had that the real API doesn't (or vice versa) — and that mismatch is exactly what step 1's shared JSON sample was meant to prevent.