MSW Mock Service Worker
Intercept HTTP in tests and dev with MSW — handlers, server setup, overrides per test, and how it beats ad-hoc fetch mocks.
- testing
- msw-mock
Mock Service Worker (MSW) intercepts requests at the network boundary. Your app uses real fetch/axios; MSW returns mocked responses. Same handlers can run in Node (tests) and the browser (dev/Storybook).
Docs: MSW, Node integration.
Why MSW
Ad-hoc vi.fn(fetch) |
MSW |
|---|---|
| Couples to client API | Works for any client |
| Easy to forget headers/status | First-class HTTP |
| Hard to share with Storybook | Shared handlers |
| URL matching DIY | Declarative http.get |
Core setup (Vitest/Jest)
// mocks/handlers.ts
import { http, HttpResponse } from 'msw';
export const handlers = [
http.get('/api/me', () =>
HttpResponse.json({ id: '1', name: 'Ada' }),
),
http.post('/api/login', async ({ request }) => {
const body = (await request.json()) as { email: string };
if (!body.email) {
return HttpResponse.json({ message: 'email required' }, { status: 400 });
}
return HttpResponse.json({ token: 'test-token' });
}),
];
// mocks/server.ts
import { setupServer } from 'msw/node';
import { handlers } from './handlers';
export const server = setupServer(...handlers);
// setupTests.ts
import { server } from './mocks/server';
beforeAll(() => server.listen({ onUnhandledRequest: 'error' }));
afterEach(() => server.resetHandlers());
afterAll(() => server.close());
onUnhandledRequest: 'error' fails tests that hit unexpected URLs — prevents silent real network calls.
Per-test overrides
server.use(
http.get('/api/me', () =>
HttpResponse.json({ message: 'unauthorized' }, { status: 401 }),
),
);
resetHandlers restores defaults after each test.
GraphQL
import { graphql, HttpResponse } from 'msw';
graphql.query('GetUser', () =>
HttpResponse.json({ data: { user: { id: '1' } } }),
);
Browser / Storybook
// browser.ts
import { setupWorker } from 'msw/browser';
import { handlers } from './handlers';
export const worker = setupWorker(...handlers);
// worker.start() in dev entry
Designers and devs can exercise error states without a backend.
Realistic delays and errors
http.get('/api/slow', async () => {
await delay(500);
return HttpResponse.json({ ok: true });
});
Use delays sparingly in unit tests; prefer fake timers for debounce.
Footguns
- Relative URL mismatch (
/apivshttp://localhost/api) — configure base or match patterns. - Leaving worker running in prod builds.
- Handlers that never assert request shape — drift from real API.
- Not resetting handlers between tests.
Interview out-loud answer
“MSW intercepts HTTP so components use real clients against mock responses. I share handlers across tests and Storybook, error on unhandled requests, and override per test for error states. It scales better than stubbing fetch on every client.”
Unhandled requests as a feature
onUnhandledRequest: 'error' turns missing handlers into failing tests — that’s desirable. When adding a new endpoint to the app, add the handler in the same PR. Treat handler files as part of the API surface the UI depends on.
Extra practice
Write a minimal demo in a scratch file or the playground: one happy path, one failure path, and one boundary input. If you cannot exhibit a bug that the pattern prevents, you do not own the concept yet — re-read the primary docs linked below and tighten the example until the failure is obvious.
Related on this site
- Mocking fetch in tests
- Integration testing UI
- Contract testing APIs
- Component testing Storybook
- Typing fetch responses
Further reading
Related guides
- Accessibility Testing in CIWire axe and lint rules into CI without false confidence — what automation catches, what it misses, and a practical pipeline.
- Component Testing StorybookUse Storybook as a component workshop and optional test runner — stories as living specs, interaction tests, and a11y checks.
- Contract Testing APIsKeep frontend and backend agreements honest — schema contracts, Pact-style consumer tests, and OpenAPI-driven checks without brittle E2E.
- Coverage Metrics PitfallsWhy 100% line coverage can still ship bugs — gaming metrics, useless tests, and what coverage is actually good for.
- End-to-End Testing TradeoffsWhen E2E tests earn their keep, why they flake, and how to keep a small critical-path suite instead of a slow second frontend.