Skip to content
Mig ration F low
frontend testing mocking msw pretender api

Pretender Mock Service Worker

Replace Pretender's in-memory fake server with MSW for network-level request interception.

Copy for

Using an AI agent? See /agents for MCP, JSON, and llms.txt access.

Pretender → Mock Service Worker

Philosophy shift

Pretender intercepts XMLHttpRequest and fetch by monkey-patching them in-memory. MSW intercepts at the network level using a Service Worker (in browsers) or Node.js interceptors — no patching, no fake server object to manage. The same handlers work in tests, the browser dev environment, and Storybook.

Rule: MSW handlers are stateless by default. Don’t reach for server.use() overrides unless a specific test needs different behavior — start from default handlers in handlers.ts and override per-test.

Setup

Remove Pretender:

npm uninstall pretender

Install MSW:

npm install --save-dev msw

Browser (dev/Storybook)

npx msw init public/ --save

This copies mockServiceWorker.js to your public/ directory.

Node (Jest / Vitest)

No extra binary needed — MSW uses Node’s http interceptors.

Core transformations

Server setup

// Pretender
import Pretender from 'pretender';

let server: Pretender;

beforeEach(() => { server = new Pretender(); });
afterEach(() => { server.shutdown(); });

// MSW
import { setupServer } from 'msw/node';
import { handlers } from './handlers';

const server = setupServer(...handlers);

beforeAll(() => server.listen());
afterEach(() => server.resetHandlers());
afterAll(() => server.close());

Defining handlers

// Pretender
server.get('/api/users', () => [
  200,
  { 'Content-Type': 'application/json' },
  JSON.stringify([{ id: 1, name: 'Alice' }]),
]);

server.post('/api/users', (request) => {
  const body = JSON.parse(request.requestBody);
  return [201, { 'Content-Type': 'application/json' }, JSON.stringify({ id: 2, ...body })];
});

// MSW
import { http, HttpResponse } from 'msw';

export const handlers = [
  http.get('/api/users', () =>
    HttpResponse.json([{ id: 1, name: 'Alice' }])
  ),

  http.post('/api/users', async ({ request }) => {
    const body = await request.json();
    return HttpResponse.json({ id: 2, ...body }, { status: 201 });
  }),
];

Reading request data

// Pretender
server.post('/api/login', (request) => {
  const { email } = JSON.parse(request.requestBody);
  const params = request.queryParams;
  const token = request.requestHeaders['Authorization'];
  return [200, {}, JSON.stringify({ token: 'abc' })];
});

// MSW
http.post('/api/login', async ({ request }) => {
  const { email } = await request.json();
  const url = new URL(request.url);
  const page = url.searchParams.get('page');
  const token = request.headers.get('Authorization');
  return HttpResponse.json({ token: 'abc' });
}),

Path parameters

// Pretender
server.get('/api/users/:id', (request) => {
  const id = request.params.id;
  return [200, {}, JSON.stringify({ id, name: 'Alice' })];
});

// MSW
http.get('/api/users/:id', ({ params }) => {
  const { id } = params;
  return HttpResponse.json({ id, name: 'Alice' });
}),

Error responses

// Pretender
server.get('/api/data', () => [500, {}, 'Internal Server Error']);

// MSW
http.get('/api/data', () =>
  new HttpResponse('Internal Server Error', { status: 500 })
),

Network errors

// Pretender — no built-in; simulate via status
server.get('/api/data', () => [0, {}, '']);

// MSW
import { http, HttpResponse } from 'msw';
http.get('/api/data', () => HttpResponse.error()),

Per-test overrides

// Pretender — redefine routes on server
server.get('/api/users', () => [200, {}, JSON.stringify([])]);

// MSW
it('shows empty state', async () => {
  server.use(
    http.get('/api/users', () => HttpResponse.json([]))
  );
  // handler is reset after this test via server.resetHandlers()
});

Passthrough (unhandled requests)

// Pretender
server.unhandledRequest = (verb, path) => {
  console.warn(`Unhandled: ${verb} ${path}`);
};

// MSW
server.listen({ onUnhandledRequest: 'warn' });
// or 'error' to fail tests on unhandled requests (recommended for CI)
server.listen({ onUnhandledRequest: 'error' });

Shared handlers file

Centralise handlers for reuse across tests, browser dev, and Storybook:

// src/mocks/handlers.ts
import { http, HttpResponse } from 'msw';

export const handlers = [
  http.get('/api/users', () => HttpResponse.json([])),
];
// src/mocks/browser.ts (for Storybook / dev)
import { setupWorker } from 'msw/browser';
import { handlers } from './handlers';
export const worker = setupWorker(...handlers);
// src/mocks/server.ts (for Node tests)
import { setupServer } from 'msw/node';
import { handlers } from './handlers';
export const server = setupServer(...handlers);

When NOT to migrate

Pitfalls

Validation checklist

Codemod references

AI Prompt

You are migrating API mocking code from Pretender to Mock Service Worker (MSW) v2.

Rules:
1. Replace `new Pretender()` setup with `setupServer(...handlers)` from msw/node.
2. Replace server.shutdown() with server.close() in afterAll.
3. Add server.resetHandlers() in afterEach.
4. Convert each server.get/post/put/delete route to an http.get/post/put/delete handler using HttpResponse.
5. Replace request.requestBody with `await request.json()` (async).
6. Replace request.queryParams with `new URL(request.url).searchParams`.
7. Replace request.params for path params with destructured `{ params }` from the handler argument.
8. Replace network error simulation with HttpResponse.error().
9. Export handlers to a separate handlers.ts file.

Migrate the following Pretender setup:

References