mrkeyoor.com_
Sun 20 Sept 12:44 UTC
npmTestingupdated 20 Sept 2026

msw review

MSW 2.15.0 answers real application requests from test-owned handlers. A browser build registers a Service Worker, while Node uses the project's interceptor layer; your code still calls fetch, Axios, or a GraphQL client. That makes one handler collection usable in component tests, integration tests, Storybook, and local browser work. Version 2.15.0 finalizes a server-sent event response when its stream ends, so cleanup can run at completion. Our full-package browser build measured 404.7 KB minified and 123.4 KB gzipped, which is too much to ship accidentally in production code.

Verdict

Our MSW 2.15.0 install used 25 MB across 60 packages, and a full browser import reached 123.4 KB gzipped, so keep its entry points in test or development code. Install it when shared network handlers are worth the worker setup; choose contract testing when response drift is the main risk.

We installed it

Lab card: what happened when we installed mswScreenshot of msw documentation
Install✓ · 5.9s60 packages on disk · 25 MB
ImportESM import works · require() works · CommonJS package with exports map
Browser123.4 KBgzipped (404.7 KB minified), bundled with esbuild
TypesTypeScript types bundled
Known vulns00 critical · 0 high · 0 moderate · 0 low (npm audit)

Answers from our run

Does msw install cleanly?

Yes. In a fresh container with an empty cache, npm install msw finished in 6 seconds, leaving 60 packages and 25 MB on disk. npm audit reported no known vulnerabilities.

How much does msw add to a browser bundle?

123.4 KB gzipped (404.7 KB minified) when the whole package is bundled for the browser with esbuild. Importing only part of it is usually smaller.

Does msw work with both ESM and CommonJS?

Yes. Both import 'msw' and require('msw') worked in Node 22 in our run. The package is published as CommonJS with an exports map.

Does msw include TypeScript types?

Yes, type declarations ship inside the package, so no @types install is needed.

msw or nock: which should you use?

nock: Use it for Node-only HTTP interception and optional request recording. Our MSW 2.15.0 install used 25 MB across 60 packages, and a full browser import reached 123.4 KB gzipped, so keep its entry points in test or development code.

When should you not use msw?

A passing test must prove compatibility with the provider; handwritten MSW payloads can drift without Pact or schema checks

API stability4/5Version 2.15.0 retains the v2 http, graphql, ws, HttpResponse, setupWorker, and setupServer shape, and its release changed SSE stream finalization without replacing those public entry points. The earlier v1 to v2 move changed resolver arguments, response construction, and imports, so old examples fail in current code. Treat major upgrades as migrations and pin the package when snapshots or timing assertions depend on interceptor behavior.
Docs5/5The official site returned HTTP 200 and documents browser registration, Node lifecycle hooks, request matching, response construction, debugging, unhandled requests, WebSockets, GraphQL, and migration from v1. Current examples use HttpResponse and environment-specific imports. Runner details can still vary around jsdom, fake timers, and package resolution, so the relevant integration page belongs beside the general quick start during setup.
Maintenance4/5The unarchived repository showed 41 open issues and pull requests and was last pushed on 2026-07-24. Release 2.15.0 shipped on 2026-07-08 with an SSE stream-finalization feature after several SSE fixes. The README openly says the project is maintained in spare time with no full-time contributor. Releases are continuing, but that staffing statement is a reason to avoid depending on rapid support for an unusual runtime edge case.
Ecosystem5/5npm counted 20,686,095 downloads from 2026-08-19 through 2026-08-25, while GitHub reported 18,168 stars. The package has dedicated browser, Node, native, HTTP, GraphQL, and WebSocket exports plus bundled TypeScript declarations. Its handlers work with application-level clients because interception happens below fetch or Axios, though that breadth brings a 123.4 KB gzipped result for our full browser import.

Use it if

  • Browser previews and Node tests need to share the same HTTP or GraphQL handlers
  • You want mocked browser traffic to remain visible in the Network panel
  • Tests need per-case delays, network errors, or temporary response overrides
  • Your suite exercises REST, GraphQL, WebSocket, or server-sent event clients
Skip it if

Setup reality

We installed MSW 2.15.0 in 5.9 seconds on Node 22. It left 60 packages occupying 25 MB, and npm audit reported 0 known vulnerabilities. The package has 18 direct dependencies and 1 peer dependency, with 7,920 KB unpacked. Bundled TypeScript types were present. Its CommonJS package and exports map loaded through require() and ESM import. Our broad browser import produced 404.7 KB minified and 123.4 KB gzipped.

Browser setup needs npx msw init pointed at the public-assets directory. Commit mockServiceWorker.js, serve it under a scope that covers the page, and regenerate it when the package tells you the worker is stale. Await worker.start() before the app sends its first request. Node tests instead import msw/node and should call listen once, resetHandlers after each case, and close once. No credential is required unless a handler itself calls a protected service.

Unhandled requests pass through by default, which can turn a typo into live network traffic. Use onUnhandledRequest: 'error' in tests that must stay offline. server.use() adds runtime handlers until resetHandlers() clears them. Request bodies follow the standard Request stream rules, so clone a request before two consumers read it. A browser Service Worker controls only URLs and pages under its registration scope.

Concurrent Node work can share handler changes unless server.boundary() or separate processes isolate them. Remove lifecycle listeners after each assertion because they accumulate on the server object. Fake timers can stall delay(). WebSocket and SSE clients also leave open handles if a test never closes them. The 2.15.0 SSE finalization change fixes cleanup at stream end, but it cannot end a client connection that the test deliberately leaves open.

Patterns

Start and stop around Node tests node-lifecycle

import { afterAll, afterEach, beforeAll } from 'vitest';
import { setupServer } from 'msw/node';
const server = setupServer(...handlers);
beforeAll(() => server.listen({ onUnhandledRequest: 'error' }));
afterEach(() => server.resetHandlers());
afterAll(() => server.close());

resetHandlers() removes overrides added by server.use(). Error mode prevents an unmatched URL from reaching the network.

Start the worker before rendering browser-worker

// once: npx msw init ./public --save
import { setupWorker } from 'msw/browser';
const worker = setupWorker(...handlers);
await worker.start();
renderApp();

The generated worker file must be served inside the page's scope. Waiting for start prevents early requests from escaping.

Handle a REST update rest-json

import { http, HttpResponse } from 'msw';
http.put('/api/users/:id', async ({ request, params }) => {
  const body = await request.json();
  return HttpResponse.json({ id: params.id, ...body });
});

request is the standard Request object. Read query parameters through new URL(request.url).searchParams.

Override one response test-override

server.use(
  http.get('/api/account', () =>
    HttpResponse.json({ message: 'down' }, { status: 503 })
  )
);

server.use() gives the new handler priority and keeps it active until resetHandlers().

Simulate a failed connection network-error

server.use(http.get('/api/account', () => HttpResponse.error()));

HttpResponse.error() rejects at the network layer. An HTTP 500 still produces a normal Response object.

Delay a response delayed-response

import { delay, http, HttpResponse } from 'msw';
http.get('/api/report', async () => {
  await delay(3000);
  return HttpResponse.json({ ready: true });
});

A fake clock can stop delay() from resolving. Advance the timer or restore real timers in that test.

Use a response once one-time-response

server.use(
  http.get('/api/job', () => HttpResponse.json({ state: 'pending' }), { once: true }),
  http.get('/api/job', () => HttpResponse.json({ state: 'done' }))
);

The once handler expires after its first match. resetHandlers() restores handlers supplied to setupServer.

Match a named GraphQL query graphql-operation

const api = graphql.link('https://api.example.com/graphql');
api.query('GetUser', ({ variables }) =>
  HttpResponse.json({ data: { user: { id: variables.id } } })
);

Operation-name matching requires the client query to be named. Anonymous operations cannot match GetUser.

Reply when a socket connects websocket-connection

import { ws } from 'msw';
const chat = ws.link('wss://chat.example.com');
chat.addEventListener('connection', ({ client }) => {
  client.send(JSON.stringify({ type: 'ready' }));
});

Close the client during cleanup or the test runner may retain an open handle.

Call the real service once bypass-interception

http.get('/api/profile', async ({ request }) => {
  const response = await fetch(bypass(request));
  return HttpResponse.json({ ...(await response.json()), preview: true });
});

bypass() marks the nested request so this handler does not catch its own fetch recursively.

Inspect a request event observe-request

const listener = ({ request }) => seen.push(request.clone());
server.events.on('request:start', listener);
await callApi();
server.events.removeListener('request:start', listener);

Clone before reading a body twice. Remove the listener or later tests will keep receiving events.

Scope handlers to one request async-boundary

app.get('/checkout', server.boundary(async (req, res) => {
  server.use(http.get('https://pay.example/session/:id', ({ params }) =>
    HttpResponse.json({ id: params.id, state: 'open' })));
  res.json(await createCheckout(req));
}));

server.boundary() isolates runtime handlers through that async context, which prevents concurrent requests from sharing overrides.

Alternatives

PackageRegistryPick it when
nocknpmUse it for Node-only HTTP interception and optional request recording.
fetch-mocknpmUse it when replacing fetch directly is enough and no browser Service Worker is needed.
miragejsnpmUse it when the mock needs models, factories, and persistent relationships.
@pact-foundation/pactnpmUse it when a consumer's examples must be checked against a provider contract.

More testing guides

pytest · chai · vitest · jsdom · playwright · coverage · the whole shelf →

How this guide is made: grounded in the library's documentation, release notes, changelog, and issue history, on a fixed rubric — not a hands-on install of every release. The 50 most-downloaded entries are additionally install-verified in clean containers. Corrections: contact the desk.