ESC

Type to search the knowledge base.

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.

intermediate3 min read
  • 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

  1. Relative URL mismatch (/api vs http://localhost/api) — configure base or match patterns.
  2. Leaving worker running in prod builds.
  3. Handlers that never assert request shape — drift from real API.
  4. 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.

Further reading

Related guides