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:

  1. Generate mock responses from the spec (the runtime returns schema-compliant data).
  2. 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

PatternBest forCost
Static JSONList/detail read-only UILowest effort
Thin Express serverPOST/PUT, paging, searchAn afternoon
USA API seededRealistic, large datasetsAPI 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:

  1. Write the OpenAPI spec for the first 5 endpoints you need.
  2. Boot the Express mock server seeded by the USA Data Tools address API.
  3. Wire the front-end to it via an API_BASE env var.
  4. Add the ?_force= hacks for slow/500/timeout scenarios.
  5. Run a spectral lint 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.