When the backend is not ready but the front-end cannot wait, a mock data API is the answer. A mock API lets the front-end team ship UI against a stable contract while the real backend catches up; lets mobile devs exercise the app without needing a live auth token; and lets nobody — but especially the QA team — wait for staging to deploy to test a new feature. This tutorial walks through three increasingly powerful mock patterns, ending with using the USA Data Tools REST API directly as a mock backend.
Why mock the API at all?
- Parallelize work: front and back teams agree on a contract; front mocks it, back implements it, both ship faster.
- Determinism: a stable mock returns the same data on every run so flaky tests go away.
- Edge cases: a mock deliberately returns 500s and 422s so your error-handling code is testable.
- Privacy: no real customer data leaves production while you iterate.
Pattern 1 — JSON files served statically
The simplest mock is a directory of JSON files served by any static HTTP server. This works for read-only endpoints.
mock-api/
users.json
users.42.json
addresses.json
Serve with anything you like — Python's http.server, VS Code Live Server, or a one-line Vite static dir. Front-end code fetches /mock-api/users.json instead of /api/users via an environment variable so you can flip the base URL later.
const API_BASE = process.env.REACT_APP_API_BASE || "/mock-api";
const users = await fetch(`${API_BASE}/users.json`).then(r => r.json());
Limitation: no per-record endpoints, no paging, no PUT/POST. The pattern covers 70% of front-end work (lists and detail views) before falling short.
Pattern 2 — A thin Node/Express server
For the rest, write a 50-line Express server that returns fixtures with realistic HTTP semantics. You get status codes, query parsing, and idempotent POST handling for free.
// mock-server/server.js
import express from "express";
import { readFileSync } from "node:fs";
const users = JSON.parse(readFileSync("./fixtures/users.json", "utf8"));
const addresses = JSON.parse(readFileSync("./fixtures/addresses.json", "utf8"));
const app = express();
app.use(express.json());
// List with paging + search
app.get("/api/users", (req, res) => {
const page = Number(req.query.page ?? 1);
const size = Number(req.query.size ?? 20);
const q = (req.query.q ?? "").toLowerCase();
const filtered = users.filter(u =>
!q || u.name.toLowerCase().includes(q) || u.email.includes(q)
);
const start = (page - 1) * size;
const data = filtered.slice(start, start + size);
res.json({
data,
page,
size,
total: filtered.length,
});
});
// Single record
app.get("/api/users/:id", (req, res) => {
const user = users.find(u => u.id === Number(req.params.id));
if (!user) return res.status(404).json({ error: "not found" });
res.json(user);
});
app.listen(8081, () => console.log("Mock API on http://localhost:8081"));
This implementation gives the front-end team realistic HTTP semantics — query parameters, status codes, content-type — at the cost of about an hour of typing.
Pattern 3 — Mock that keeps generating fresh data
Static fixtures go stale; the 10 test rows from last sprint stop stressing the infinite-scroll pagination you just built. Use the USA Data Tools API to populate your mock on demand.
// fetch 50 fresh US records whenever the mock server boots
async function seedAddresses(count = 50) {
const r = await fetch(
`https://vic999.com/us-address/api/v1/address?count=${count}`,
{ headers: { Authorization: `Bearer ${process.env.USA_KEY}` } }
);
if (!r.ok) throw new Error(`seed failed: ${r.status}`);
const { data } = await r.json();
return data.map((a, i) => ({ id: i + 1, ...a }));
}
let cache = [];
app.get("/api/addresses", async (req, res) => {
if (cache.length === 0) cache = await seedAddresses(50);
res.json({ data: cache });
});
On the first request the mock lazily seeds 50 realistic addresses (geographically consistent ZIP+state, valid area-code phones) and then serves them from cache. Restarting the mock gives you a fresh batch — useful when you want determinism-by-default but a different shape each sprint.
Serving edge-case responses
A mock that only ever returns 200 OK does not exercise error paths. Branch on a query parameter so your tests can opt into failure modes:
app.get("/api/users/:id", (req, res) => {
if (req.query._force === "500") {
return res.status(500).json({ error: "forced_error" });
}
if (req.query._force === "slow") {
return setTimeout(() => res.json(users[0]), 5000);
}
const user = users.find(u => u.id === Number(req.params.id));
if (!user) return res.status(404).json({ error: "not_found" });
res.json(user);
});
Now your front-end tests can request ?_force=500 to verify the error-boundary UI appears, or ?_force=slow to verify the spinner shows up. This pattern alone closes most front-end resilience gaps.
Documenting the mock contract
The biggest cost of a mock is drift between the mock and the real backend. Pair the mock server with an OpenAPI spec — even hand-authored — and validate both directions:
- Generate mock responses from the spec (the runtime returns schema-compliant data).
- Validate the real backend against the spec in CI (a cheap contract test).
Once both the mock and the real backend speak the same schema, the front-end does not care which one it is talking to.
npx @stoplight/spectral-cli lint openapi.yaml --ruleset spectral:oas
Putting it in CI
jobs:
front-end-e2e:
runs-on: ubuntu-latest
services:
mock:
image: node:20
options: --entrypoint node
env:
USA_KEY: ${{ secrets.USA_KEY }}
ports: ["8081:8081"]
steps:
- uses: actions/checkout@v4
- run: npm ci
- run: npm run start:mock &
- run: npx playwright test
- run: npx @stoplight/spectral-cli lint openapi.yaml
The mock boots as a service, the front-end tests run against it, and a contract check warns if the spec drifts. When the real backend lands, flip the env var and the same tests pass once the contract is honest.
Choosing between three approaches
| Pattern | Best for | Cost |
|---|---|---|
| Static JSON | List/detail read-only UI | Lowest effort |
| Thin Express server | POST/PUT, paging, search | An afternoon |
| USA API seeded | Realistic, large datasets | API key + 50 lines |
Cross-team handoff
When the backend team is ready, the front-end mocks become the regression test suite. Their spectral-validated spec becomes the contract; the front-end Playwright suite runs against the real backend and exposes any drift ("backend added a required field"). This is why contracts matter more than mocks — but mocks let you start exercising the contract on day one.
Tip: mock servers are short-lived by design. When the real backend lands, do not throw the mock away — keep it running on a localhost-only port for fast offline iteration and for deliberately triggered failure cases that the real backend will not produce on demand.
Practical recipe for a greenfield SaaS
If you are starting from scratch:
- Write the OpenAPI spec for the first 5 endpoints you need.
- Boot the Express mock server seeded by the USA Data Tools address API.
- Wire the front-end to it via an
API_BASEenv var. - Add the
?_force=hacks for slow/500/timeout scenarios. - Run a
spectrallint as a contract test in both the mock and the real backend's CI.
This pattern has held up for years across our projects and scales from a single front-end dev to a 20-person team without changing the architecture.
Wrapping up
A mock data API is not a hack — it is a deliverable contract between teams that lets everyone move in parallel. Start with a few JSON files, escalate to an Express server when you need HTTP semantics, and seed it with the address generator for datasets that actually exercise edge cases. Pair it with a Spectral-validated OpenAPI spec and the same suite runs against the real backend when it is ready with zero rewrites.