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: ACC portal claims API
The ACC 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 an ACC 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 ACC 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. Teams building portals for government agencies (MSD, ACC, IRD) often use MSW to develop and test against simulated API responses while backend contracts are still being negotiated under the GCDO API standard. For NZ QA engineers working in JavaScript stacks, understanding MSW is a practical advantage — it is the test infrastructure you will encounter when joining a React-based project at companies like Xero, Trade Me, or ANZ Digital.
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 ACC 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.