Mocking APIs in tests is painful. MSW (Mock Service Worker) intercepts requests at the network level — same mocks work in tests, Storybook, and development.
The Problem
// Traditional mocking — tightly coupled, breaks easily
jest.mock("../api/users", () => ({ getUsers: jest.fn().mockResolvedValue([]) }));
// What if the component uses fetch directly?
// What if it goes through axios?
// What if the URL changes?
MSW Solution: Mock the Network
// mocks/handlers.ts
import { http, HttpResponse } from "msw";
export const handlers = [
// GET /api/users
http.get("/api/users", () => {
return HttpResponse.json([
{ id: 1, name: "Alice", email: "alice@test.com" },
{ id: 2, name: "Bob", email: "bob@test.com" },
]);
}),
// POST /api/users
http.post("/api/users", async ({ request }) => {
const body = await request.json();
return HttpResponse.json({ id: 3, ...body }, { status: 201 });
}),
// Error responses
http.get("/api/users/:id", ({ params }) => {
if (params.id === "999") {
return HttpResponse.json({ message: "Not found" }, { status: 404 });
}
return HttpResponse.json({ id: params.id, name: "User" });
}),
];
These handlers work regardless of whether your code uses fetch, axios, ky, or any other HTTP client.
In Tests (Vitest/Jest)
// mocks/server.ts
import { setupServer } from "msw/node";
import { handlers } from "./handlers";
export const server = setupServer(...handlers);
// setup.ts
beforeAll(() => server.listen());
afterEach(() => server.resetHandlers());
afterAll(() => server.close());
// UserList.test.tsx
import { render, screen } from "@testing-library/react";
import { server } from "../mocks/server";
import { http, HttpResponse } from "msw";
test("shows users", async () => {
render(<UserList />);
expect(await screen.findByText("Alice")).toBeInTheDocument();
expect(screen.getByText("Bob")).toBeInTheDocument();
});
test("handles error", async () => {
// Override handler for this test only
server.use(
http.get("/api/users", () => {
return HttpResponse.json(null, { status: 500 });
})
);
render(<UserList />);
expect(await screen.findByText("Failed to load users")).toBeInTheDocument();
});
In Browser (Development)
// mocks/browser.ts
import { setupWorker } from "msw/browser";
import { handlers } from "./handlers";
export const worker = setupWorker(...handlers);
// main.tsx
if (process.env.NODE_ENV === "development") {
const { worker } = await import("./mocks/browser");
await worker.start();
}
Your app runs with mocked APIs — build frontend before backend is ready.
In Storybook
import { http, HttpResponse } from "msw";
export const WithUsers: Story = {
parameters: {
msw: {
handlers: [
http.get("/api/users", () =>
HttpResponse.json([{ id: 1, name: "Storybook User" }])
),
],
},
},
};
Advanced Features
Network-Level Assertions
const createUser = http.post("/api/users", async ({ request }) => {
const body = await request.json();
// MSW 2.0: passthrough to real API in specific cases
if (body.email.endsWith("@real.com")) {
return passthrough();
}
return HttpResponse.json({ id: 1, ...body });
});
Streaming Responses
http.get("/api/stream", () => {
const stream = new ReadableStream({ ... });
return new HttpResponse(stream, {
headers: { "Content-Type": "text/event-stream" },
});
});
Building APIs or need robust testing infrastructure? I create developer tools and data solutions. Email spinov001@gmail.com or explore my Apify tools.
Top comments (0)