MSW (Mock Service Worker)
MSW is a browser and Node.js API mocking library that intercepts real requests at the network level using Service Workers (in browsers) and interceptors (in Node.js). It lets you mock HTTP and GraphQL requests without changing your application code.
Overview
Mock Service Worker (MSW) was created by Artem Zakharchenko in 2019 and has become the de facto standard for API mocking in the JavaScript ecosystem. Instead of monkey-patching fetch or wrapping axios, MSW registers a Service Worker in the browser that acts as a network-level proxy. In Node.js (for Jest or Vitest tests), MSW uses an interceptor layer instead of a Service Worker — but the handler API is identical. You write your mock handlers once and they work in both environments.
MSW version 2 (released late 2023) introduced a cleaner handler API with http and HttpResponse, better TypeScript support, and first-class ESM compatibility. Version 2 is the current standard and what this page documents.
Why MSW beats manual mocks
Manual mocks duplicate knowledge. You write both the real API call and the mock in two places. When the real API changes, the mock silently lies — your tests keep passing while the actual integration is broken. This false confidence is worse than no mock at all.
MSW intercepts at the network level. Your component code calls fetch or axios exactly as it would in production. MSW intercepts the request before it leaves the browser. Your tests exercise the real request-making logic — the same headers, the same error handling, the same retry logic — because nothing about the call path changes. Only the response is substituted.
| Approach | Intercepts real requests | Production-identical code path | Maintained centrally |
|---|---|---|---|
| Manual mock | No | No | No |
| Axios/fetch mock | Partially | No | Sometimes |
| MSW | Yes | Yes | Yes |
| WireMock | Yes (separate process) | Yes | Yes |
When to use MSW
✓ Use MSW when
- Writing component or integration tests that make real HTTP calls
- Testing loading states, error states, and retry logic without a live server
- Doing frontend development before the backend API exists
- Replacing
axios-mock-adapter,jest.mock('axios'), or__mocks__files in an existing Jest or Vitest suite - Sharing the same mock handlers between tests and a local development server (
msw/browser)
✗ Use something else when
- You need to test the real API — use contract testing (Pact) or end-to-end tests against a live environment instead
- You need a fully stateful mock server with persistent data, sequences, and record-replay — use WireMock
- You are testing a Node.js backend that calls downstream APIs — MSW works here, but WireMock or a test double at the HTTP adapter layer may be cleaner
- Your team is Java or .NET-first — WireMock has a richer ecosystem and tooling for those stacks
Setup in 5 minutes
Install MSW as a dev dependency:
npm install msw --save-dev
Define your handlers in a central file. Handlers describe which requests to intercept and what response to return:
// src/mocks/handlers.js
import { http, HttpResponse } from "msw";
export const handlers = [
// GET — return a KiwiSaver balance
http.get("https://api.example.co.nz/kiwisaver/balance", () => {
return HttpResponse.json({
balance: 45230.87,
lastUpdated: "2026-06-27",
memberId: "KS-123456"
});
}),
// POST — validate a contribution and return a transaction ID
http.post("https://api.example.co.nz/kiwisaver/contribution", async ({ request }) => {
const body = await request.json();
if (!body.amount || body.amount <= 0) {
return new HttpResponse(null, { status: 422 });
}
return HttpResponse.json({ success: true, transactionId: "TXN-789" });
}),
];
For Jest / Vitest (Node.js environment), set up a server in your test setup file:
// src/mocks/server.js
import { setupServer } from "msw/node";
import { handlers } from "./handlers";
// This server starts/stops around each test suite automatically
export const server = setupServer(...handlers);
// vitest.setup.js (or jest.setup.js)
import { server } from "./src/mocks/server";
import { afterAll, afterEach, beforeAll } from "vitest";
beforeAll(() => server.listen({ onUnhandledRequest: "error" }));
afterEach(() => server.resetHandlers());
afterAll(() => server.close());
For the browser (development workflow with Vite or CRA), initialise a Service Worker:
// Generate the Service Worker file (run once)
npx msw init public/ --save
// src/mocks/browser.js
import { setupWorker } from "msw/browser";
import { handlers } from "./handlers";
export const worker = setupWorker(...handlers);
// src/main.js — start the worker in development only
if (import.meta.env.DEV) {
const { worker } = await import("./mocks/browser");
await worker.start();
}
onUnhandledRequest: "error" — set this in your test server to make unmatched requests throw. This prevents tests from accidentally hitting real APIs and makes missing handlers obvious immediately rather than silently returning undefined.
NZ-specific use case: CoverNZ portal claims API
The CoverNZ Claims Management System exposes a /api/claims/status endpoint that returns the current state of an injury claim. During development, this API may not yet exist, or the backend team may not have finished implementing all status transitions. MSW lets the frontend team build and test the claims status UI in full, against all possible states, before the real API is available.
Define handlers that return different claim states based on the claim ID in the request URL:
// src/mocks/handlers.js
import { http, HttpResponse } from "msw";
const claimFixtures = {
"CLM-001": { claimId: "CLM-001", status: "Pending", label: "Claim received — awaiting assessment" },
"CLM-002": { claimId: "CLM-002", status: "Under Review", label: "Being assessed by a CoverNZ case manager" },
"CLM-003": { claimId: "CLM-003", status: "Approved", label: "Claim accepted. Weekly compensation active." },
"CLM-004": { claimId: "CLM-004", status: "Declined", label: "Claim declined. See letter for details." },
};
export const handlers = [
http.get("/api/claims/status/:claimId", ({ params }) => {
const claim = claimFixtures[params.claimId];
if (!claim) {
return new HttpResponse(null, { status: 404 });
}
return HttpResponse.json(claim);
}),
];
Test that the UI renders the correct status badge for each claim state:
// ClaimStatus.test.jsx
import { render, screen } from "@testing-library/react";
import { ClaimStatus } from "./ClaimStatus";
// The MSW server is running via vitest.setup.js — no extra setup needed here.
const statusCases = [
{ claimId: "CLM-001", expectedBadge: "Pending" },
{ claimId: "CLM-002", expectedBadge: "Under Review" },
{ claimId: "CLM-003", expectedBadge: "Approved" },
{ claimId: "CLM-004", expectedBadge: "Declined" },
];
test.each(statusCases)(
"renders $expectedBadge badge for claim $claimId",
async ({ claimId, expectedBadge }) => {
render(<ClaimStatus claimId={claimId} />);
// The component calls fetch("/api/claims/status/CLM-001") — MSW intercepts it
const badge = await screen.findByRole("status");
expect(badge).toHaveTextContent(expectedBadge);
}
);
The component's fetch call runs unchanged. MSW intercepts the network request, returns the fixture, and the test asserts on what the user actually sees. When the real CoverNZ API is ready, you point the component at it and remove the handler — the tests remain valid because they tested the component, not the mock.
MSW vs WireMock
Which mock server belongs where?
MSW — best for frontend
- Component and integration tests using Jest, Vitest, or React Testing Library
- Development workflow — run the browser worker so your UI works without a backend
- No separate process: handlers are JavaScript files that live next to your component code
- Works in browser (Service Worker) and Node.js (interceptor) from the same handler file
- Lightweight: ~20 KB in production bundle when excluded correctly
WireMock — best for backend integration
- Full integration tests where you need a stateful mock server with persistent data
- Java and Spring Boot backends testing downstream REST dependencies
- Separate JVM process: acts as a real HTTP server on a port — any language can call it
- Record-and-replay: capture real API responses and replay them in CI
- Better support for complex stub sequencing, delays, and fault injection
Both are better than: manual fetch mocks, axios-mock-adapter, jest.mock() on HTTP clients, or __mocks__ files. Those approaches bypass the real request path and create a second codebase of mock knowledge that drifts from reality.
Pros & Cons
Pros
- Intercepts at the network level — your application code is identical to production
- One handler file works in browser and Node.js (Jest/Vitest)
- Excellent TypeScript support with typed request/response bodies
- Handlers can be overridden per-test with
server.use()for one-off scenarios - Free and open source; actively maintained with strong community support
- Works with any HTTP client:
fetch,axios,ky,got,SWR, React Query
Cons
- JavaScript/TypeScript ecosystem only — not a fit for Python, Java, or .NET projects
- Service Worker requires HTTPS in production (not an issue in test environments)
- Not stateful by default — simulating sequences of state changes requires extra handler logic
- MSW v2 has breaking changes from v1 — existing codebases need migration
- GraphQL handler support is less mature than HTTP handler support
Platforms & Integrations
MSW runs on Windows, macOS, and Linux. It is a JavaScript-only library with no runtime dependencies.
Pricing
| Tier | Cost | Includes |
|---|---|---|
| Open Source | Free | Full library, all features, browser + Node.js support, community support on GitHub |
NZ Context
MSW is increasingly common in NZ frontend teams using React or Vue. NZ government agencies building citizen-facing portals often adopt MSW to develop and test against simulated API responses while backend contracts are still being negotiated under the GCDO API standard — a pattern that matters particularly when the backend is a legacy system accessible only inside the agency network. NZ SaaS companies and NZ financial services organisations building React-based dashboards and self-service portals have similarly adopted MSW as the standard way to decouple frontend development from API availability. For NZ QA engineers working in JavaScript stacks, understanding MSW is a practical advantage — it is the test infrastructure you will encounter when joining most modern React or Vue projects in the NZ market.
Alternatives
- WireMock — Stateful HTTP mock server, best for Java/Spring Boot. Runs as a separate process. Better for full integration test suites.
- json-server — Quickly spin up a REST API from a JSON file. Stateful but not production-identical; no request interception.
- Mirage JS — In-browser API mocking with an ORM-like model layer. Good for prototyping but heavier than MSW and less maintained.
- Nock — Node.js HTTP mocking library. Intercepts Node.js HTTP/HTTPS calls. Does not work in the browser; no shared handler file.
Self-check
Click each question to reveal the answer.
Q1: What is the core difference between MSW and a manual fetch mock or jest.mock('axios')?
MSW intercepts at the network level — your component calls fetch exactly as it would in production, and MSW intercepts the request before it leaves the browser or Node.js process. A manual mock replaces the function itself, so your test never exercises the real request-making code: headers, interceptors, retry logic, and error handling are all bypassed. MSW tests the real code path; a manual mock tests a substitute.
Q2: How does MSW work differently in a browser vs a Jest/Vitest (Node.js) test environment?
In the browser, MSW registers a Service Worker (a background script that proxies network traffic). The Service Worker intercepts outgoing requests and returns handler responses without them reaching the real server. In Node.js, MSW uses a low-level HTTP interceptor that patches Node's http and https modules. The handler API — the http.get() and http.post() calls — is identical in both environments. You write handlers once and they work in tests and in the browser development workflow.
Q3: Why should you set onUnhandledRequest: "error" when setting up the MSW server in tests?
Without this option, any request that does not match a handler silently passes through or returns undefined. This means a test can call a real external API (bypassing the mock layer entirely) or call a non-existent endpoint and receive no useful error. Setting onUnhandledRequest: "error" makes MSW throw an error for any request with no matching handler, so missing mocks fail loudly and immediately rather than causing subtle false-passing tests.
Q4: When would you choose WireMock over MSW?
WireMock is the better choice when: you need a stateful mock server (data that persists across requests and changes state), you are testing a Java or Spring Boot backend that calls downstream REST services, you want record-and-replay of real API interactions, or you need a fully independent HTTP server that any language can call. MSW is JavaScript-only and best for frontend component tests and development workflow. For full backend integration test suites, WireMock is typically the cleaner fit.
Q5: In the CoverNZ claims example, if you removed the MSW handler and the test still passed, what would that tell you?
It would mean the test is not actually making an HTTP call — or the request is being handled by some other intercept (a residual jest.mock, a cached response, or the request silently returning undefined because onUnhandledRequest is not set to "error"). If the test passes without the handler, it is not testing the component's network behaviour at all. This is exactly the scenario MSW with onUnhandledRequest: "error" is designed to prevent: you would get an error on the unmatched request, making the dependency on the handler explicit.
When to choose MSW (Mock Service Worker)
A quick decision guide for NZ teams evaluating api mocking options.
| Choose MSW (Mock Service Worker) when… | Choose something else when… | Combine with… |
|---|---|---|
| Your team writes React, Vue, or Svelte and tests with Jest or Vitest — MSW plugs in with zero adapter code | The service under test is a Java or Spring Boot backend calling downstream APIs — use WireMock, which runs as a real JVM HTTP server any language can target | Pact — use MSW for fast isolated component tests; use Pact to verify the contract between your frontend and the real provider on every CI run |
| The backend API is still being designed or negotiated — MSW lets the frontend team build against a stable handler while contracts are in flux | You need a stateful mock that accumulates data across requests (e.g., order history that grows with each POST) — use WireMock with stateful scenarios or json-server for simple REST state | React Testing Library — MSW handles the network layer; RTL handles the DOM assertion layer. Together they give you full component integration tests without a browser |
You want to share the same handler file between unit tests and a local development server — the same handlers.js drives both msw/node in Jest and msw/browser in Vite dev mode |
Your team is primarily .NET or Python — MSW is JavaScript only; consider WireMock.Net or Python's responses library instead | Vitest — faster than Jest for TypeScript projects; MSW's Node interceptor works identically in both test runners, so switching is a config change, not a handler rewrite |
You are migrating away from jest.mock('axios'), axios-mock-adapter, or fetchMock — MSW replaces all three with one approach that exercises the real HTTP code path |
You need record-and-replay of real production traffic for regression testing — WireMock's record mode captures real API responses; MSW has no equivalent built in | Playwright — use MSW for unit and integration tests; use Playwright's page.route() or a real staging environment for end-to-end browser tests where a Service Worker is impractical |
What I would do
Practitioner judgment on tool adoption, team onboarding, and when to swap.
If…
I were joining a CloudBooks team building a new React dashboard that calls CloudBooks's own public API — a setup where the frontend and API evolve in parallel and the team already uses Vitest
I would…
Introduce MSW on day one, not after the first round of flaky tests. Set up a single handlers/ directory at the project root, wire it to both Vitest's setup file and Vite's dev server, and set onUnhandledRequest: "error" from the start. The discipline of defining a handler before writing a test is what prevents the silent-passing-test problem that haunts teams using jest.mock. I would also add a lint rule banning jest.mock on HTTP clients — the two approaches conflict, and MSW should win every time.
If…
I were on a ListRight QA team that had built a large Jest test suite using axios-mock-adapter and was seeing regular test failures caused by mock drift — cases where the real ListRight Listings API had changed shape but the mocks had not been updated
I would…
Migrate to MSW incrementally — one endpoint at a time, starting with the ones that break most often. I would not attempt a big-bang rewrite. Run MSW alongside axios-mock-adapter for a sprint, prove the coverage is equivalent, then remove the old adapter. Once MSW is in place, I would also introduce Pact contract tests for the five highest-risk endpoints so that any future change to the real Listings API breaks a Pact test in CI before it breaks a test in production.
If…
I were advising an Revenue NZ development team building a self-service tax portal in Next.js where the backend is a legacy COBOL-era core system exposed through a thin REST facade — a system where the API is slow, access-controlled, and unavailable outside the Revenue NZ internal network
I would…
Treat MSW as the gateway to being able to develop and test at all outside the Revenue NZ network. Map out the ten or fifteen core API shapes the portal depends on, build fixture files for each from real response samples (sanitised of any personal data), and commit them to the repository. This means any developer can run the portal locally without a VPN connection and any CI runner can execute the full test suite. The handler file also becomes the living documentation of what the legacy API actually returns — something that rarely exists otherwise in teams working on inherited government systems.
The bottom line: MSW earns its place not just as a test utility but as the shared contract between your frontend team and whatever API they depend on. Treat the handler file like a first-class artefact — version it, review it, and keep it as close to the real API shape as you can. The teams that get the most value from MSW are the ones who stopped thinking of it as "mocking" and started thinking of it as "local API documentation that also runs tests."
Interview questions
Questions you are likely to get if you list MSW on your CV — with what interviewers are really testing for.
Why would you choose MSW over jest.mock('axios') or a manual fetch stub? What does the network-level interception actually buy you?
What they’re really testing: Whether you understand the difference between replacing a function and intercepting a real network call — and whether you can explain why false-passing tests are worse than failing ones.
Strong answer covers: Manual mocks replace the HTTP client so headers, interceptors, and retry logic are never exercised; MSW lets the real fetch or axios call fire and intercepts at the network layer so the full request path is tested; mention onUnhandledRequest: "error" as the safeguard that prevents silent test drift.
When would you use WireMock instead of MSW, and when would you use both on the same project?
What they’re really testing: Whether you can place tools in context rather than treating the first tool you learned as the universal answer.
Strong answer covers: MSW is JavaScript-only and best for frontend component tests and dev workflow; WireMock runs as a separate JVM process and is the right fit for Java/Spring Boot backends testing downstream REST dependencies or needing record-and-replay; on a full-stack NZ SaaS project you might use both — MSW for the React test suite and WireMock for the backend integration layer.
You’re joining an Revenue NZ team building a Next.js self-service portal. The legacy backend REST API is only accessible inside the Revenue NZ network and is too slow to hit in CI. How would you structure your MSW setup?
What they’re really testing: Whether you can apply the tool to a realistic government-sector constraint — not just toy examples — and whether you think about fixture management and team workflow, not just test syntax.
Strong answer covers: Map the core API shapes the portal depends on and build sanitised fixture files committed to the repo; wire the same handler file to both msw/node in the test suite and msw/browser in Vite dev mode so any developer can run locally without VPN; treat the handler file as living API documentation for a system that often has no other spec.
Your MSW-backed component tests pass on every developer machine but fail consistently in CI with “Network request failed” errors. What do you investigate first?
What they’re really testing: Whether you can debug infrastructure issues rather than just test logic — and whether you know the difference between browser and Node.js MSW setup.
Strong answer covers: Check that the test setup file (vitest.setup.js or jest.setup.js) is registered in the runner config and that server.listen() runs in beforeAll; confirm the CI environment is using Node.js MSW (msw/node) not the browser worker; check that afterEach(() => server.resetHandlers()) is present so per-test overrides from one test don’t bleed into the next.
How would you structure MSW handlers as a CloudBooks team scales from 5 to 50 component tests? What patterns prevent the handler file from becoming unmaintainable?
What they’re really testing: Whether you think about handler architecture at scale, not just syntax — and whether you’ve hit the maintenance problems that come from a single flat handlers array.
Strong answer covers: Split handlers by domain into separate files (e.g. handlers/accounts.js, handlers/invoices.js) and compose them at the server setup; use server.use() for per-test overrides so the base handler stays clean; co-locate fixture data with the handler that uses it rather than a single giant fixtures file; enforce that nobody imports handlers directly in test files — all overrides go through server.use().