Mock Network Requests with MSW
How to use Mock Service Worker (MSW) to intercept network requests in JavaScript tests and development, including REST and GraphQL mocking.
Overview
Mock Service Worker (MSW) intercepts network requests at the Service Worker level in browsers and at the fetch/http level in Node.js. Instead of patching modules or starting a mock server, MSW declares request handlers that return mock responses. The same handlers work in development, tests, and Storybook. If you only need simpler HTTP stubs without Service Worker semantics, see the lighter-weight API mocking recipe.
When to Use
- Mocking API responses during frontend development without a backend
- Testing components that fetch data with
fetch,axios, or@tanstack/react-query - Testing GraphQL queries and mutations without a running GraphQL server
- Sharing mocks between Storybook, unit tests, and integration tests
- Developing frontend features before the backend API is ready
When NOT to Use
- Testing your own backend API — use
supertestorWebTestClient - Testing real network behavior — MSW intercepts before the network layer
- Load testing — MSW adds interception overhead
- Testing WebSocket connections — MSW 2.x supports it, but it’s less mature than HTTP mocking
Solution
Setup
npm install -D msw
Basic REST handlers
import { http, HttpResponse } from "msw";
export const handlers = [
http.get("https://api.example.com/users/:id", ({ params }) => {
return HttpResponse.json({
id: Number(params.id),
name: "Alice",
email: "alice@example.com",
});
}),
http.post("https://api.example.com/users", async ({ request }) => {
const body = await request.json();
return HttpResponse.json(
{ id: 42, ...body },
{ status: 201 },
);
}),
http.get("https://api.example.com/users", ({ request }) => {
const url = new URL(request.url);
const page = url.searchParams.get("page") || "1";
return HttpResponse.json({
data: [{ id: 1, name: "Alice" }, { id: 2, name: "Bob" }],
page: Number(page),
total: 2,
});
}),
];
Using MSW in Node.js tests (Vitest)
The same pattern works with Jest — only the lifecycle hook names differ. For snapshot-oriented Vitest workflows, see Snapshot Testing with Vitest.
import { setupServer } from "msw/node";
import { afterAll, afterEach, beforeAll, describe, it, expect } from "vitest";
import { handlers } from "./handlers";
const server = setupServer(...handlers);
beforeAll(() => server.listen({ onUnhandledRequest: "error" }));
afterEach(() => server.resetHandlers());
afterAll(() => server.close());
describe("User API", () => {
it("fetches a user by ID", async () => {
const response = await fetch("https://api.example.com/users/1");
const user = await response.json();
expect(user.id).toBe(1);
expect(user.name).toBe("Alice");
});
it("creates a user", async () => {
const response = await fetch("https://api.example.com/users", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ name: "Charlie", email: "charlie@example.com" }),
});
expect(response.status).toBe(201);
const user = await response.json();
expect(user.id).toBe(42);
expect(user.name).toBe("Charlie");
});
});
GraphQL handlers
import { graphql, HttpResponse } from "msw";
export const graphqlHandlers = [
graphql.query("GetUser", ({ variables }) => {
return HttpResponse.json({
data: {
user: {
id: variables.id,
name: "Alice",
email: "alice@example.com",
},
},
});
}),
graphql.mutation("CreateUser", ({ variables }) => {
return HttpResponse.json({
data: {
createUser: {
id: 99,
...variables.input,
},
},
});
}),
];
Simulating errors
http.get("https://api.example.com/users/1", () => {
return HttpResponse.json(
{ error: "Not found" },
{ status: 404 },
);
});
http.get("https://api.example.com/server-error", () => {
return HttpResponse.json(
{ error: "Internal Server Error" },
{ status: 500 },
);
});
Simulating network errors
import { http, HttpResponse, delay } from "msw";
http.get("https://api.example.com/slow", async () => {
await delay(5000);
return HttpResponse.json({ data: "delayed" });
});
http.get("https://api.example.com/network-error", () => {
return HttpResponse.error();
});
Overriding handlers per test
it("handles 500 error", async () => {
server.use(
http.get("https://api.example.com/users/1", () => {
return HttpResponse.json({ error: "Server error" }, { status: 500 });
}),
);
const response = await fetch("https://api.example.com/users/1");
expect(response.status).toBe(500);
});
Using MSW in the browser (development)
import { setupWorker } from "msw/browser";
import { handlers } from "./handlers";
const worker = setupWorker(...handlers);
await worker.start({
onUnhandledRequest: "bypass",
});
console.log("MSW worker started");
Generate the service worker file:
npx msw init public/ --save
Verifying requests
import { http, HttpResponse } from "msw";
let lastRequestBody: unknown;
export const captureHandler = http.post(
"https://api.example.com/orders",
async ({ request }) => {
lastRequestBody = await request.json();
return HttpResponse.json({ id: 1 }, { status: 201 });
},
);
// In test
it("sends correct order payload", async () => {
await submitOrder({ productId: 10, quantity: 3 });
expect(lastRequestBody).toEqual({ productId: 10, quantity: 3 });
});
Explanation
MSW doesn’t fake your HTTP client — it sits underneath it. That’s why fetch, axios, and @tanstack/react-query all work without configuration: they funnel into the same interception points.
In the browser, msw init public/ writes a mockServiceWorker.js file and setupWorker registers it. The worker catches requests through the fetch event, so DevTools shows them like real network calls — you can inspect headers and payloads as usual. In Node.js there’s no Service Worker API, so msw/node patches fetch, http, https, and XMLHttpRequest directly.
Handler resolution has one rule worth remembering: handlers registered with server.use() are prepended, so a per-test override always wins over the shared handlers array, and resetHandlers() in afterEach removes those overrides so they never leak between tests.
Migrating from MSW 1.x? The API changed quite a bit: rest.get() plus ctx.json() became http.get() returning HttpResponse.json(), res() is gone, and WebSocket mocking moved to ws.link(). All examples in this recipe use the 2.x API.
Variants
Using MSW with React Testing Library
import { render, screen, waitFor } from "@testing-library/react";
import { UserProfile } from "./UserProfile";
it("displays user data from API", async () => {
server.use(
http.get("/api/users/1", () => {
return HttpResponse.json({ name: "Alice", email: "alice@example.com" });
}),
);
render(<UserProfile userId={1} />);
await waitFor(() => {
expect(screen.getByText("Alice")).toBeInTheDocument();
});
});
Using MSW with Storybook
// .storybook/preview.tsx
import { setupWorker } from "msw/browser";
import { handlers } from "../src/mocks/handlers";
const worker = setupWorker(...handlers);
await worker.start();
export const parameters = {
msw: { handlers },
};
For a tighter integration, msw-storybook-addon gives you initialize() plus an mswLoader so each story can override handlers declaratively instead of starting the worker by hand.
Best Practices
- For a deeper guide, see Stub External HTTP Services with WireMock — the JVM equivalent for stubbing external HTTP services.
- Share handlers between tests, Storybook, and development — one source of truth for mock data
- Use
server.resetHandlers()inafterEachto clear per-test overrides - Set
onUnhandledRequest: "error"in tests to catch unmocked requests - Use
delay()to test loading states and timeout handling - Keep handlers in a separate
handlers.tsfile for reusability - Use
server.use()for one-off overrides instead of modifying shared handlers
Common Mistakes
- Not calling
server.listen()inbeforeAll: MSW won’t intercept requests until the server starts. - Not resetting handlers: per-test overrides leak into subsequent tests. Always call
resetHandlers(). - Mocking relative URLs in Node.js tests: use absolute URLs or configure a base URL. MSW in Node.js needs full URLs.
- Forgetting
awaitin async handlers:HttpResponse.json()is synchronous, but reading the request body withrequest.json()is async. - Not closing the server: call
server.close()inafterAllto prevent the process from hanging.
See Also
- API Mocking
- Snapshot Testing with Vitest
- Testing Strategy Guide
- MSW documentation
- Companion files — runnable handlers, Vitest setup and Storybook config
Frequently Asked Questions
How do I mock file uploads?
http.post("/api/upload", async ({ request }) => {
const formData = await request.formData();
const file = formData.get("file") as File;
return HttpResponse.json({ filename: file.name, size: file.size });
});
Can I use MSW with axios?
Yes. MSW intercepts at the fetch and http level, which axios uses internally. No configuration needed.
How do I mock paginated responses?
http.get("/api/users", ({ request }) => {
const url = new URL(request.url);
const page = Number(url.searchParams.get("page") || 1);
const allUsers = Array.from({ length: 50 }, (_, i) => ({ id: i + 1 }));
const perPage = 10;
const start = (page - 1) * perPage;
return HttpResponse.json({
data: allUsers.slice(start, start + perPage),
page,
total: 50,
});
});
Does MSW work with WebSocket?
MSW 2.x supports WebSocket mocking via ws handler:
import { ws } from "msw";
const chat = ws.link("wss://api.example.com/chat");
export const handlers = [
chat.addEventListener("connection", ({ client }) => {
client.send("Welcome!");
}),
];
How do I debug unmocked requests?
Set onUnhandledRequest: "warn" to see warnings in the console, or "error" to fail tests on unmocked requests. Check the console output for the full URL and method.
Related Resources
Vitest Snapshot Testing for React
How to use Vitest snapshot testing to catch unintended UI changes in React components, including inline snapshots and snapshot update workflows.
RecipeTest Express APIs with supertest
How to test Express.js REST API endpoints end-to-end using supertest, including status codes, JSON bodies, headers, authentication, and error handling.
RecipeAPI Mocking for Testing
Build reliable tests by mocking external APIs with WireMock, MockServer, and MSW to eliminate flakiness and test edge cases.
GuideSoftware Testing Strategy Guide
A practical guide to building a layered testing strategy with unit, integration, and end-to-end tests.