How to Mock API Requests with MSW

Use Mock Service Worker to intercept network calls in tests so React components get realistic responses without a real backend.

7 min read

Mock Service Worker, or MSW, lets you mock API requests at the network level so React components receive realistic responses without a real server. This guide shows how to mock API requests with MSW in a Vitest test suite. The component keeps its real fetch code while the network is controlled from the test side.

Install and write handlers

Install MSW as a development dependency.

bashbash
npm install -D msw

A request handler declares which request to match and what to respond. Handlers live in one file that tests and local development can share.

index.jsindex.js
// src/mocks/handlers.js
import { http, HttpResponse } from "msw";
 
export const handlers = [
  http.get("/api/user", () => {
    return HttpResponse.json({ name: "Ada" });
  }),
];

The http namespace describes the request method and URL. HttpResponse builds the response body, so the mocked endpoint returns the same shape a real server would send. Defining handlers once and reusing them across tests keeps the mock in one place instead of scattering stubs through every file.

Start a mock server for Node

Tests run in Node, so the setupServer function from msw/node patches the fetch and http modules that the component uses.

index.jsindex.js
// src/mocks/node.js
import { setupServer } from "msw/node";
import { handlers } from "./handlers";
 
export const server = setupServer(...handlers);

The server does nothing until it is started. Patching the request layer means the component's own fetch call stays untouched, which keeps tests close to production behavior. A Vitest setup file starts it before all tests, resets handlers between tests, and closes it at the end.

index.jsindex.js
// src/setupTests.js
import { beforeAll, afterEach, afterAll } from "vitest";
import { server } from "./src/mocks/node";
 
beforeAll(() => server.listen());
afterEach(() => server.resetHandlers());
afterAll(() => server.close());

Register this file in the test setupFiles array of your Vitest config. From then on, every test shares the same mock server and starts with clean handlers. If the setup file is not registered, the server never starts and every request hits the real network.

Test a component against the mock

A profile component fetches from the mocked endpoint without knowing it is mocked.

App.jsxApp.jsx
import { useEffect, useState } from "react";
export default function Profile() {
  const [name, setName] = useState(null);
  useEffect(() => {
    let ignore = false;
    fetch("/api/user").then((r) => r.json()).then((data) => {
      if (!ignore) setName(data.name);
    });
    return () => { ignore = true; };
  }, []);
  if (name === null) return <p>Loading profile</p>;
  return <h1>{name}</h1>;
}

The test below renders the component and waits for the name that the handler returns. Nothing about the component changes, because the mock lives at the network boundary.

App.jsxApp.jsx
import { render, screen } from "@testing-library/react";
import Profile from "./Profile";
 
test("shows the name returned by the API", async () => {
  render(<Profile />);
 
  expect(await screen.findByText("Ada")).toBeVisible();
});

The component performs a real fetch, MSW intercepts it, and the handler responds with Ada. The test sees the final UI, which is what a user would see. For the async utilities behind the wait, review how to test async React components.

Override a response for one test

Sometimes a single test needs a different response, such as an empty list or an error. Use server.use to add a temporary handler inside that test.

App.jsxApp.jsx
import { http, HttpResponse } from "msw";
import { render, screen } from "@testing-library/react";
import { server } from "./src/mocks/node";
import UserList from "./UserList";
 
test("shows a message when there are no users", async () => {
  server.use(
    http.get("/api/users", () => HttpResponse.json([]))
  );
 
  render(<UserList />);
 
  expect(await screen.findByText("No users yet")).toBeVisible();
});

The new handler wins for that test only. resetHandlers in the afterEach hook removes it before the next test runs. This keeps each test's response explicit at the point where it matters.

Common mistakes

  • Patching fetch by hand with vi.fn when MSW would keep the real request path intact.
  • Forgetting resetHandlers, so one test leaks its override into the next.
  • Registering the setup file but not starting the server with listen.
  • Mocking the response shape differently from the real API, so tests pass against a lie.

What to learn next

MSW pairs with the fetch components covered in the async testing guide, and the runner setup lives in how to test React components with Vitest.

Rune AI

Rune AI

Key Insights

  • Install msw and define handlers with http and HttpResponse.
  • Create a server with setupServer from msw/node.
  • Start, reset, and close the server in a Vitest setup file.
  • Override responses per test with server.use.
  • Mock the network layer so components keep their real fetch code.
RunePowered by Rune AI

Frequently Asked Questions

Does MSW replace my real backend in production?

No. MSW runs in tests and local development only. Production keeps its real API. The browser worker is registered in development, never shipped to users.

Can I change a mock response for one test?

Yes. Call server.use with a new handler inside the test, then server.resetHandlers in afterEach restores the shared handlers.

Conclusion

MSW mocks the network layer instead of a single function. Define handlers once, start a server for tests, and override responses per test with server.use.