MSW (Mock Service Worker) Guide
The complete reference for Mock Service Worker (MSW) — the network-level API mocking library trusted by the React, Testing Library, and Storybook communities. Covers handlers, browser and Node.js setup, GraphQL mocking, and testing strategies.
What is Mock Service Worker?
Mock Service Worker (MSW) is an API mocking library that operates at the network layer rather than patching JavaScript modules. Traditional mocking libraries replace fetch or axios with a mock implementation — which means your tests use a different code path than your production app. MSW takes a different approach: your application code calls fetch('/api/users') normally, and MSW intercepts that real HTTP request before it reaches the network.
In the browser, MSW registers a Service Worker that intercepts outgoing requests. In Node.js (Jest, Vitest), it patches the built-in http module. Crucially, both environments use the same handler syntax — define your mock API once in a handlers.ts file and reuse it in development (browser), unit tests (Vitest/Jest), and integration tests (Playwright). This eliminates the drift between test mocks and production behaviour.
Installation
# Install MSW npm install msw --save-dev # Initialize the service worker file (browser) npx msw init public/ --save # Verify the service worker is in public/ ls public/mockServiceWorker.js
Defining Request Handlers
Handlers are the heart of MSW. Define them in a shared file so they can be imported by both the browser worker and the Node.js server.
// src/mocks/handlers.ts
import { http, HttpResponse, delay } from 'msw';
export const handlers = [
// GET request — return JSON
http.get('/api/users', () => {
return HttpResponse.json([
{ id: 1, name: 'Alice', role: 'admin' },
{ id: 2, name: 'Bob', role: 'user' },
]);
}),
// GET with URL params
http.get('/api/users/:id', ({ params }) => {
const { id } = params;
if (id === '999') {
return HttpResponse.json({ message: 'User not found' }, { status: 404 });
}
return HttpResponse.json({ id: Number(id), name: 'Alice' });
}),
// POST — read request body
http.post('/api/users', async ({ request }) => {
const body = await request.json() as { name: string; email: string };
return HttpResponse.json({ id: 3, ...body }, { status: 201 });
}),
// Simulate slow API
http.get('/api/slow-endpoint', async () => {
await delay(2000);
return HttpResponse.json({ data: 'delayed response' });
}),
// Simulate network failure
http.get('/api/broken', () => {
return HttpResponse.error();
}),
]; Browser Setup: setupWorker
// src/mocks/browser.ts
import { setupWorker } from 'msw/browser';
import { handlers } from './handlers';
export const worker = setupWorker(...handlers);
// src/main.tsx — start only in development
async function enableMocking() {
if (process.env.NODE_ENV !== 'development') return;
const { worker } = await import('./mocks/browser');
return worker.start({
onUnhandledRequest: 'warn', // Warn on unmocked requests
});
}
enableMocking().then(() => {
ReactDOM.createRoot(document.getElementById('root')!).render(<App />);
}); Node.js Setup: setupServer (Jest/Vitest)
// src/mocks/server.ts
import { setupServer } from 'msw/node';
import { handlers } from './handlers';
export const server = setupServer(...handlers);
// tests/setup.ts — configure in vitest.config.ts or jest.config.ts
import { server } from '../src/mocks/server';
import { afterAll, afterEach, beforeAll } from 'vitest'; // or jest
beforeAll(() => server.listen({ onUnhandledRequest: 'error' }));
afterEach(() => server.resetHandlers()); // Reset per-test overrides
afterAll(() => server.close()); Testing Different API States
import { render, screen, waitFor } from '@testing-library/react';
import { server } from '../mocks/server';
import { http, HttpResponse } from 'msw';
import { UserList } from './UserList';
describe('UserList', () => {
it('displays users from API', async () => {
render(<UserList />);
await waitFor(() => {
expect(screen.getByText('Alice')).toBeInTheDocument();
expect(screen.getByText('Bob')).toBeInTheDocument();
});
});
it('shows empty state when no users', async () => {
// Override handler for this specific test
server.use(
http.get('/api/users', () => HttpResponse.json([]))
);
render(<UserList />);
await waitFor(() => {
expect(screen.getByText('No users found')).toBeInTheDocument();
});
});
it('shows error state on server failure', async () => {
server.use(
http.get('/api/users', () =>
HttpResponse.json({ message: 'Server error' }, { status: 500 })
)
);
render(<UserList />);
await waitFor(() => {
expect(screen.getByText('Something went wrong')).toBeInTheDocument();
});
});
}); GraphQL Mocking
import { graphql, HttpResponse } from 'msw';
export const handlers = [
graphql.query('GetUser', ({ variables }) => {
return HttpResponse.json({
data: {
user: { id: variables.id, name: 'Alice', email: '[email protected]' },
},
});
}),
graphql.mutation('CreateUser', ({ variables }) => {
return HttpResponse.json({
data: {
createUser: { id: '99', ...variables.input },
},
});
}),
]; Key Features
- Network-level interception — works with any HTTP client: fetch, axios, ky, SWR, React Query
- Unified handler syntax — same handlers.ts used in browser dev and Node.js tests
- REST and GraphQL — first-class support for both API styles
- Response utilities — HttpResponse.json(), delay(), error(), network simulation
- Per-test overrides — server.use() overrides specific handlers for edge case tests
- TypeScript first — fully typed request params, body, and response
- Storybook integration — use MSW handlers as mock API in stories
Comparison: MSW vs Alternatives
| Feature | MSW | axios-mock-adapter | nock | fetch-mock |
|---|---|---|---|---|
| Network level | Yes (Service Worker) | No (patches axios) | Node.js http only | No (patches fetch) |
| Browser support | Yes | With axios | No | Yes |
| Node.js support | Yes | With axios | Yes | Yes |
| GraphQL support | Built-in | No | No | No |
| HTTP client agnostic | Yes | axios only | Node http only | fetch only |
| Storybook integration | msw-storybook-addon | No | No | No |
Pros & Cons
Pros
- Intercepts at network level — no patching of fetch/axios
- Same handlers work in browser, Vitest, Jest, and Storybook
- Works with any HTTP client without configuration
- First-class GraphQL support with operation name matching
- Endorsed by Testing Library and React Query communities
- TypeScript-first with excellent type inference
Cons
- Service Worker setup required for browser (one-time)
- MSW v2 has breaking API changes from v1
- More setup than simple jest.mock() for simple cases
- Browser worker requires HTTPS in some environments
- Cannot inspect request count without custom logging
Frequently Asked Questions
What is Mock Service Worker (MSW) and how does it work?▼
Mock Service Worker is an API mocking library that intercepts network requests at the Service Worker level in browsers, and at the Node.js http module level in test environments. Unlike traditional mocking approaches that patch fetch or axios directly, MSW intercepts at the network level — your application code makes real HTTP requests that are intercepted by MSW before reaching the server. This means the same handler code works in browser development, browser tests, and Node.js tests.
What is the difference between setupWorker and setupServer?▼
setupWorker is for browser environments — it registers a Service Worker at the root scope that intercepts outgoing requests. Call worker.start() and worker.stop() to control it. setupServer is for Node.js environments (Jest, Vitest, Playwright) — it uses node-intercept to patch the native http/https module. Both share the same request handler syntax (http.get, http.post) so you can reuse handlers across environments by importing them from a shared handlers.ts file.
How do MSW request handlers work?▼
Handlers are created with http.get(url, resolver) or http.post(url, resolver) where the resolver is an async function that receives request info and returns an HttpResponse. The URL can be an absolute URL, a relative path, or a glob pattern. The resolver receives a RequestContext with request (the Request object), params (URL params), and cookies. Return HttpResponse.json(data) for JSON, HttpResponse.text(body) for plain text, or HttpResponse.error() to simulate a network failure.
Can MSW mock GraphQL APIs?▼
Yes. MSW has a graphql namespace alongside http. Use graphql.query('GetUser', resolver) to handle a specific query by operation name, or graphql.operation(resolver) to handle all GraphQL operations. The resolver receives the query, variables, and request. This lets you mock GraphQL APIs with the same colocation approach as REST — one handlers.ts file used across development and tests. MSW's GraphQL support works with Apollo Client, URQL, and any fetch-based client.
How do I test different API states (loading, error, empty) with MSW?▼
Override handlers at the test level using server.use(). Call server.use(http.get('/api/users', () => HttpResponse.json([], { status: 200 }))) before the test that needs an empty state, or HttpResponse.json({ message: 'Not found' }, { status: 404 }) for error states. Call server.resetHandlers() in afterEach to restore the default handlers defined in setupServer(). This pattern makes it trivial to test every network state without any special test helpers.
Is MSW a replacement for unit test mocks like jest.mock()?▼
MSW and jest.mock() serve different layers. jest.mock() replaces JavaScript modules — useful for mocking utility functions, third-party libraries, or anything that doesn't involve HTTP. MSW intercepts HTTP requests — ideal for any code that calls fetch, axios, or any HTTP client. In a well-structured app, service layer functions make HTTP calls; mock those calls with MSW and leave the service function un-mocked. This tests the full request/response serialization path, not just the business logic.
Related Products
on GumroadDevToolkit Starter Kit
A ready-to-deploy developer tools website with 12 built-in tools. Astro + Preact + Tailwind. Deploy to Cloudflare Pages for free. Built-in SEO, dark mode, and ad-ready layout.
Buy on Gumroad →TypeScript Quick Reference Toolkit
5 practical reference files for TypeScript developers: cheatsheet, TS vs JS comparison, 20 utility types with real examples, tsconfig best practices template, and 15 common mistakes guide. Print it. Keep it on your desk.
Buy on Gumroad →Developer Productivity Bundle
Stop rebuilding the same setup every project. 51 VSCode snippets, 40 Git aliases, 5 Docker Compose stacks, 5 GitHub Actions workflows, 5 README templates, and a dotfiles kit. Set up a professional dev environment in under an hour.
Buy on Gumroad →You Might Also Need
OpenAPI Validator
Paste your OpenAPI 3.x spec (YAML or JSON) to validate structure, find missing required fields, detect duplicate paths, check for missing operationIds, and get errors and warnings with path references.
Open free tool →
API Response Mock Generator
Generate realistic mock API responses with custom status codes, headers, and JSON body. Quick templates for 200, 401, 404, 429, 500 and more. Runs in your browser.
Open free tool →
API Response Time Tester
Test API endpoint response times directly from your browser. Run multiple requests, see average/min/max times, status codes, and response sizes. GET, POST, PUT, DELETE support. Free.
Open free tool →
More Free Tools
Related Articles
MSW 2.0: The Best Way to Mock APIs in Frontend Tests
A complete guide to Mock Service Worker (MSW) 2.0 — setup, request handlers, Vitest and Jest integration, Storybook mocking, and best practices for realistic API mocking in frontend tests.
API Mocking for Frontend Development: Complete Guide (2025)
How to use API mocking to develop frontend apps without a real backend. Covers MSW, JSON Server, mock services in Postman, browser-based mock APIs, and testing strategies.
API Testing Best Practices 2026: Postman vs Insomnia vs Bruno
Complete guide to API testing in 2026. Compare Postman, Insomnia, and Bruno, learn best practices for REST and GraphQL testing, and build automation pipelines that catch regressions before production.
Vitest Unit Testing: Complete Setup Guide for 2026
Learn Vitest from zero to production: installation, test syntax, mocking, coverage reports, Vitest UI, and Vitest vs Jest migration. Practical guide for Vite and non-Vite projects.
Vitest Complete Guide: Setup, Unit Testing, Mocking, and Coverage in TypeScript
The complete Vitest guide for TypeScript projects. Learn how to set up Vitest, write unit and integration tests, use mocks and spies, configure coverage, and integrate with React and Node.js — with 10+ practical examples.
Enjoying this tool? Get more like it.
Weekly developer tool guides, productivity playbooks & AI tricks. Free. No spam.
Unlock AI-Powered Dev Tools
- ⚡ AI Code Review, Doc Generator & SQL Builder
- ⚡ All premium templates & early access
- ⚡ Member discounts on Gumroad products
$9/mo after trial · Cancel anytime
Want the full toolkit?
Get DevPlaybook Pro
Every template, guide, boilerplate, and automation script in one bundle. 13 premium products — grab them all at once and save big.
MIT licensed · Instant download · No subscription
See what's included → Browse all deals →