Executive Definition
API mocking is the practice of intercepting HTTP or gRPC requests inside a test environment and answering them with fixture data, without ever reaching the production service. Where a stub replaces a function call inside your process, an API mock intercepts the outgoing socket write itself. The application code is unchanged; it still calls fetch('/api/orders'), but the response is served by a mock server running in-process, in a sidecar, or inside the service worker.
The distinction matters because modern applications rarely depend on a single external service. A checkout page might touch a pricing API, a tax API, an inventory API, and a payment gateway — each with its own latency profile, error surface, and staging quota. Running the full stack in a test environment is expensive and slow. API mocking flips the model: the test declares exactly which endpoints exist, what they return, and how they fail, and the application runs against that contract.
Three tools dominate the space in 2026: Mock Service Worker (MSW) for browser and Node, WireMock for JVM back-ends, and Prism for OpenAPI-driven mocks. All three share the same core idea: a request matcher and a response definition. The differences are ergonomic — MSW uses TypeScript handlers, WireMock uses JSON stub mappings, Prism generates handlers from your OpenAPI file automatically.
API mocks are most valuable at two layers of the test pyramid. In component and integration tests, they replace the network so a React or Vue component can be rendered with realistic data without spinning up a back-end. In end-to-end tests, they replace only the true externalities — payment providers, email services, third-party APIs with rate limits — while the rest of the stack runs for real. Mocking your own back-end in an E2E test is usually a mistake: you end up testing the mock instead of the system.
Treat every mock as a signed contract. If the mocked endpoint returns a shape that the real endpoint no longer produces, your tests are lying. The best teams generate mock payloads from the same schema (OpenAPI, GraphQL SDL, Protobuf) that generates the client, and run a scheduled contract test against the real endpoint to detect drift. Without that discipline, API mocking silently becomes a way to freeze a stale view of the world.
Architecture & Production Code
A typical MSW setup sits between the application and the network. In the browser it registers a service worker; in Node it patches the fetch and http modules. Requests are matched against handlers, and unmatched requests either pass through or throw, depending on config.
┌──────────────────┐ fetch('/api/orders') ┌────────────────────┐
│ React component │ ───────────────────────────▶ │ MSW request │
└──────────────────┘ │ interceptor │
└─────────┬──────────┘
│
match ────────▶│
│
┌────────────────────┴────────────────────┐
│ │
▼ ▼
┌──────────────────┐ ┌────────────────────┐
│ handler(orders) │ │ onUnhandled: warn │
│ returns fixture │ │ or passthrough │
└────────┬─────────┘ └────────────────────┘
│
▼
┌──────────────────┐
│ Response object │
└──────────────────┘The service-worker approach is decisive: because MSW intercepts at the network layer, the application uses its real fetch client, real request headers, real serialization. Anything that would work against a real server works against the mock. That property is what makes MSW handlers safe to share between unit, component, and end-to-end tests.
The handler is where the intelligence lives. A handler can inspect the request body, throw a 500 on the third call to simulate a retry, delay the response to test loading states, or vary the payload based on a query parameter. The response is not static; it is programmable, and that programmability is what separates API mocks from JSON fixtures.
Unhandled requests are the operator's fault, not the mock's. Configure MSW to log a warning (development) or fail the test (CI) when the app hits an endpoint no handler covers. Silent passthrough is the setting that lets stale mocks rot undetected.
// src/test/mocks/handlers.ts
import { http, HttpResponse } from "msw";
export const handlers = [
http.get("/api/orders", ({ request }) => {
const url = new URL(request.url);
const status = url.searchParams.get("status");
return HttpResponse.json({
orders: [
{ id: "o_1", status: status ?? "paid", total: 4200 },
{ id: "o_2", status: status ?? "paid", total: 1180 },
],
});
}),
http.post("/api/orders", async ({ request }) => {
const body = (await request.json()) as { total: number };
if (body.total <= 0) {
return HttpResponse.json({ error: "invalid_total" }, { status: 422 });
}
return HttpResponse.json({ id: "o_new", ...body }, { status: 201 });
}),
];
// src/test/setup.ts
import { setupServer } from "msw/node";
import { handlers } from "./mocks/handlers";
export const server = setupServer(...handlers);
beforeAll(() => server.listen({ onUnhandledRequest: "error" }));
afterEach(() => server.resetHandlers());
afterAll(() => server.close());
// OrdersList.test.tsx
import { render, screen } from "@testing-library/react";
import OrdersList from "./OrdersList";
it("renders orders returned by the API", async () => {
render(<OrdersList />);
expect(await screen.findByText(/o_1/)).toBeInTheDocument();
expect(screen.getByText(/\$42\.00/)).toBeInTheDocument();
});API Mocking vs Stubbing vs Fake Server
| Aspect | API mock (MSW) | Function stub | Fake server (Docker) |
|---|---|---|---|
| Layer intercepted | HTTP / network | Function call | Full HTTP server |
| Setup time per test | Milliseconds | Milliseconds | Seconds |
| Realism of transport | High (real fetch) | Low (bypasses fetch) | Highest |
| Works in browser + Node | Yes | Yes | Node only |
| Shared with E2E? | Yes | No | Yes |
| Best when | Component and integration tests | Pure unit tests | Contract or perf tests |
API mocking wins whenever you want a real HTTP round-trip without a real server. Reach for function stubs only inside pure unit tests where the network is irrelevant, and reach for a fake server (WireMock, testcontainers) when you specifically need to exercise transport, TLS, or connection pooling.
Production Debugging Scenarios
API mocks fail in three distinctive ways. Recognising them saves hours of guessing why a green suite lied.
Handler matches too broadly and hides regressions
- Symptom
- Every test that hits /api/* returns the same fixture even after the endpoint changed.
- Root cause
- A catch-all handler using http.get('/api/*') was left over from an early scaffold.
- Fix
- Replace wildcard handlers with explicit paths. Add onUnhandledRequest: 'error' so new endpoints surface immediately.
Service worker not registered in the browser test
- Symptom
- Playwright test hits the real staging API instead of the mock.
- Root cause
- The service worker file was missing from the built assets folder.
- Fix
- Run msw init public/ to publish mockServiceWorker.js, then call worker.start() inside the app entry when import.meta.env.MODE === 'test'.
Mocks drift from the real contract
- Symptom
- Component tests pass; production returns 500 because the mocked payload omits a new required field.
- Root cause
- Handlers were hand-written and never regenerated when the OpenAPI schema evolved.
- Fix
- Generate handlers from OpenAPI (msw-auto-mock, orval) and run a nightly contract test against the real endpoint to detect drift.
Practice this concept in a real QA interview
Run a live mock with our AI Interview Coach, tune your resume with the ATS Resume Reviewer, and screen live listings on the QA Jobs Radar.