DEV Community

Alex Spinov
Alex Spinov

Posted on

MSW Has a Free API Mocking Library That Works Everywhere

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?
Enter fullscreen mode Exit fullscreen mode

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" });
  }),
];
Enter fullscreen mode Exit fullscreen mode

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());
Enter fullscreen mode Exit fullscreen mode
// 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();
});
Enter fullscreen mode Exit fullscreen mode

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();
}
Enter fullscreen mode Exit fullscreen mode

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" }])
        ),
      ],
    },
  },
};
Enter fullscreen mode Exit fullscreen mode

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 });
});
Enter fullscreen mode Exit fullscreen mode

Streaming Responses

http.get("/api/stream", () => {
  const stream = new ReadableStream({ ... });
  return new HttpResponse(stream, {
    headers: { "Content-Type": "text/event-stream" },
  });
});
Enter fullscreen mode Exit fullscreen mode

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)