Skip to main content

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.

✓ Network-level interception ✓ Browser + Node.js support ✓ REST & GraphQL ✓ Reuse handlers across environments ✓ Works with any HTTP client

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

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.

Share this tool: X / Twitter LinkedIn
✓ 60,000+ developers use DevPlaybook ★★★★★ 4.8 / 5 on Gumroad ✓ MIT licensed · Instant download ✓ No login required

Related Products

on Gumroad

You Might Also Need

More Free Tools

Enjoying this tool? Get more like it.

Weekly developer tool guides, productivity playbooks & AI tricks. Free. No spam.

DevPlaybook Pro 7 days free

Unlock AI-Powered Dev Tools

  • ⚡ AI Code Review, Doc Generator & SQL Builder
  • ⚡ All premium templates & early access
  • ⚡ Member discounts on Gumroad products
Start Free Trial →

$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.

$79
$241 separately Save $162 (67% off)
Get DevPlaybook Pro — $79 →

MIT licensed · Instant download · No subscription

See what's included → Browse all deals →