Skip to content
faker.tools
All 79

Guide · 11 min read

Mocking REST APIs for Frontend Development: MSW, json-server, Prism and Playwright

Four ways to build a frontend before — or without — its backend, what each one is good at, and how to keep mocks from drifting away from the real API.

Key takeaways

  • Pick the mock by where it runs: static fixtures for unit tests, MSW for the browser and Node, json-server for a quick standalone CRUD API, Prism when you have an OpenAPI spec, and Playwright routes for end-to-end tests.
  • MSW intercepts requests at the network layer, so your application code calls the real fetch and does not know it is being mocked.
  • Mock the unhappy paths deliberately: 401, 404, 422, 500, slow responses, empty lists and the last page of pagination are where frontends break.
  • Mocks drift from the real API. Generate them from, or validate them against, the OpenAPI spec — or add contract tests — so a mock cannot quietly lie.

Frontend work blocks on the backend more often than it needs to. The endpoint is not built yet, or staging is down, or the one error you need to design for only happens when a third-party service times out. A mock API removes the dependency: the frontend talks to something that answers like the real API, on your schedule.

The tools differ mainly in where the mock runs, and that decides what each is good for.

Option 1: static fixtures

The simplest mock is a JSON file imported into a test:

import users from "./fixtures/users.json";

render(<UserTable users={users} />);

Fixtures are perfect for component and unit tests: no network, no setup, fully deterministic. Generate them with the JSON mock data generator rather than typing them, so they contain realistic variety — long names, nulls, Unicode — instead of "John Doe" three times.

Their limit is that the component under test never makes a request, so loading states, retries and error handling go untested.

Option 2: Mock Service Worker (MSW)

MSW intercepts requests at the network level: a service worker in the browser, a request interceptor in Node. Your application calls the real fetch, and MSW decides what comes back. The same handlers work in local development, in Vitest or Jest, and in Storybook.

// mocks/handlers.ts
import { http, HttpResponse, delay } from "msw";
import users from "./fixtures/users.json";

export const handlers = [
  http.get("/api/v1/users", async () => {
    await delay(300); // make loading states visible
    return HttpResponse.json({ data: users, meta: { total: users.length } });
  }),

  http.get("/api/v1/users/:id", ({ params }) => {
    const user = users.find((u) => String(u.id) === params.id);
    return user
      ? HttpResponse.json(user)
      : HttpResponse.json({ error: { code: "not_found" } }, { status: 404 });
  }),

  http.post("/api/v1/users", async ({ request }) => {
    const body = (await request.json()) as Record<string, unknown>;
    return HttpResponse.json({ id: users.length + 1, ...body }, { status: 201 });
  }),
];

In the browser, generate the worker script once with npx msw init public/, then start it with setupWorker(...handlers).start() from msw/browser before your app renders. In tests, use setupServer(...handlers) from msw/node and call listen(), resetHandlers() and close() around your suite.

The real strength is overriding a handler for a single test:

server.use(
  http.get("/api/v1/users", () => HttpResponse.json({ error: "boom" }, { status: 500 }))
);

Option 3: json-server

json-server turns a JSON file into a REST API with full CRUD:

npx json-server db.json

A db.json with a users array gives you GET /users, GET /users/1, POST /users, PATCH /users/1 and DELETE /users/1 on http://localhost:3000, with filtering, sorting and pagination through query parameters. Writes are saved back to the file, so the API behaves statefully. The query-parameter syntax changed in version 1, so check the README for the version you install.

It is the fastest route to a working backend for a prototype. It is less suited to testing failure modes, because every response is a success unless you add middleware.

Option 4: Prism, from an OpenAPI spec

If your API has an OpenAPI document, Prism serves it directly:

npx @stoplight/prism-cli mock openapi.yaml       # examples from the spec, on port 4010
npx @stoplight/prism-cli mock -d openapi.yaml    # dynamic, schema-valid random data

Prism also validates every incoming request against the spec and returns errors for invalid ones, which catches frontend bugs — a missing required field, a wrong type — before the real backend exists. Because the mock is derived from the contract, it cannot drift from it.

Option 5: Playwright route interception

For end-to-end tests, Playwright can intercept requests per test without any extra server:

test("shows an error when the user list fails", async ({ page }) => {
  await page.route("**/api/v1/users", (route) =>
    route.fulfill({ status: 503, json: { error: "Service Unavailable" }, headers: { "Retry-After": "30" } })
  );
  await page.goto("/admin/users");
  await expect(page.getByRole("alert")).toContainText("try again");
});

Cypress offers the same through cy.intercept(), including { fixture: "users.json" } shorthand.

Choosing

Need Use
Component and unit tests Static fixtures, or MSW in Node
Local development without a backend MSW in the browser, or json-server
A quick CRUD prototype json-server
You have an OpenAPI spec Prism
End-to-end tests of rare failures Playwright route or Cypress intercept

Mock the unhappy paths

A mock that only returns 200 OK tests the screen that was already going to work. The ones that break in production are these:

  • 401 and 403 — expired sessions and missing permissions. Does the app redirect, or show a blank page?
  • 404 — a deleted record opened from an old link.
  • 422 — validation errors returned per field. Are they shown next to the right input?
  • 429 with Retry-After — does the client back off?
  • 500 and 503 — a friendly error, a retry button, no infinite spinner.
  • Slow responses — loading states that appear and disappear without layout shift.
  • Empty lists — the empty state most designs forget.
  • The last page — a partial page, a disabled "next" button, correct totals.

The REST API response simulator produces full responses — status line, headers and body — for each of these, and the pagination generator produces offset, cursor, Link-header and JSON:API pagination envelopes, including the empty page.

Keep mocks honest

The risk of any mock is that it keeps answering the old way after the real API changes. Three defences, from cheapest to strongest:

  1. Generate the mock from the spec (Prism), so there is nothing to drift.
  2. Validate mock responses against the spec in CI, using a JSON Schema or OpenAPI validator on your fixture files.
  3. Contract tests such as Pact, where the frontend's expectations are recorded and verified against the real provider.

Even with all three, keep a small suite of end-to-end tests against a real backend with seeded data. Test data for end-to-end tests covers how to make that suite reliable.

Frequently asked questions

What is the best way to mock an API in React or Vue?

Mock Service Worker (MSW) is the most common choice today. It intercepts fetch and XMLHttpRequest with a service worker in the browser and a request interceptor in Node, so the same handlers serve local development, unit tests and Storybook without changing application code.

What is the difference between MSW and json-server?

json-server runs a separate HTTP server that turns a JSON file into a CRUD API, and persists changes back to the file. MSW runs inside your app or test process and returns whatever your handlers return. json-server is quicker to start; MSW gives you full control over status codes, delays and edge cases.

How do I mock an API from an OpenAPI specification?

Use Prism: npx @stoplight/prism-cli mock openapi.yaml starts a mock server on port 4010 that returns the examples in your spec, or random schema-valid data with the -d flag. It also validates incoming requests against the spec.

Should end-to-end tests use mocks?

Some of them. Mocked end-to-end tests are fast and deterministic, and they are the only practical way to test rare failures. Keep a smaller set that runs against a real backend with seeded data, so integration problems are still caught.

Tools this guide covers

Full directory →