A Comprehensive Guide to Mock Service Worker (MSW)

Mock Service Worker is an API mocking tool that lets you mock by intercepting requests on the network level. You can reuse the same mock definition for testing, development, and debugging. 

MSW is delightful to adopt. Plus, it makes testing components with network requests a pleasure. 

In this article, I’d like to share some best practices for working with MSW for basic and more advanced problems.

When to use a Mock Service Worker?

Has this ever happened to you?

You’ve finally gotten the green light to spend some writing tests! 

This sprint is calm, but you remembered all too clearly that disastrous release a few weeks ago. The entire team had to drop everything and fix bugs. 

Your repo has around 20% test coverage, and everyone agrees that it’s too little, but there’s never enough time to write tests. You write your features, get an LGTM on the PR, and move on. 

But this release will be different! 

You have a dedicated task, a couple of story points, and a jug of coffee to get this done right. You know the feature you’re about to test very well; you wrote most of it. You set out to mock the component and pass some props. 

The tests fail. 

No worries, that was only the first try, my friend! You re-check what needs testing, add some missing mocks, and maybe throw a jest.spyOn in there. You’re so proud of your accomplishment!

yarn test – another fail.

You dig deeper and realize your component depends on a network request returning some data. 

Ok, don’t panic.

Let’s see how other tests in the codebase manage this. You find tests mocking axios, other ones' mock fetch, and wait; there are also some global files mocking resolved values and network errors?

You sweat a little, add many lines of code to your test, and ponder briefly if all of them are necessary. You notice tests unrelated to your changes started failing! You start feeling like the people in the first half of any infomercial.

…and you probably think: there has to be a better way! That’s when Mock Service Worker comes in handy.

The basic guide to Mock Service Worker (MSW)

Mock Service Worker intercepts real network requests that your tests make. 

The components in your tests think they are talking with the real API, which means you can test their real functionality (as opposed to testing contained, mock functionality). If you don’t feel convinced about this approach just yet, I invite you to read Kent C. Dodd’s excellent blog post: Stop Mocking Fetch.

Let’s see this magical library in action. As with any new dependency, you need to start by adding it to your project: npm install msw --save-devor yarn add msw --dev

Once the installation finishes, you will have to define your request handlers. MSW documentation suggests creating them in a file called mocks/handlers.js. Depending on your project, you can create REST or GraphQL handlers. MSW request handlers aim to hold instructions for specific API endpoints.

Let’s take a look at an example REST request handler:

// src/mocks/handlers.js
import { rest } from "msw";
export const handlers = [
// Handles a POST request
rest.post("https://fancy-app.com/postToThisEndpoint", (req, res, ctx) => {
return res(
// Respond with a 200 status code
ctx.status(200)
);
}),
// Handles a GET request
rest.get("https://fancy-app.com/getSomeData", (req, res, ctx) => {
return res(
ctx.status(200),
ctx.json({
data: "fancy data string",
})
);
}),
];
view raw msw.test.tsx hosted with ❤ by GitHub

We start by importing the rest function from MSW, then create an array of API endpoints we want to handle. 

We can add anything we would like to this array. We can mock an error response; we can mock correct or faulty data objects. Remember to use absolute request URLs since we will use those request handlers in a NodeJS environment.

The only caveat here is that if you add multiple handlers for one API endpoint, the last one in the array will be the only one that’s actually used. But more on that later.

Once you have a few handlers ready, it is time to let your test suite know about them. We will need to set up a request mocking server and run it with our tests. This may sound scary, but don’t worry; the server setup file is three lines long. 

Let’s create a new file called server.js

// src/mocks/server.js
import { setupServer } from 'msw/node'
import { handlers } from './handlers'
// This configures a request mocking server with the given request handlers.
export const server = setupServer(...handlers)
view raw msw.test.2.tsx hosted with ❤ by GitHub

We have to import the setupServer function from msw/node and pass the previously created request handlers to that function.

I don’t know about you, but this is the simplest mock server setup I have ever seen!

The very last step is running the mock server when you run your tests. 

This step depends heavily on your project setup. There are various ways to set up and configure Jest, and this blog post will not try to cover all of them.

Instead, I’ll talk about the setup I’ve seen the most often: using a dedicated setupTests.js file, which is called through Jest’s setupFilesAfterEv.

If that’s the case in your project, congratulations! You only need to add the following lines:

// src/setupTests.js
import { server } from './mocks/server.js'
// Establish API mocking before all tests.
beforeAll(() => server.listen())
// Reset any request handlers that we may add during the tests,
// so they don't affect other tests.
afterEach(() => server.resetHandlers())
// Clean up after the tests are finished.
afterAll(() => server.close())
view raw msw.test.2.tsx hosted with ❤ by GitHub

If you prefer to test MSW quickly in a single test file, you can do so as well. 

It is possible to use the mock server directly anywhere you would like. This means you can skip all the steps above (except for the installation, let’s not get carried away!) and write something like this in your test file:

// __tests__/FancyPage.test.js
import { setupServer } from "msw/node";
import { FancyComponentWithAPICall } from "../FancyComponentWithAPICall";
const server = setupServer(
rest.get("https://fancy-app.com/getSomeData", (req, res, ctx) => {
return res(ctx.json({ data: "return this string" }));
})
);
describe("<FancyComponentWithAPICall />", () => {
beforeAll(() => server.listen());
afterAll(() => server.close());
test("displays data from backend", async () => {
// Render components, perform requests, receive mocked responses.
});
});
view raw msw.test.4.tsx hosted with ❤ by GitHub

This has all been fun and games so far, don’t you think? 

Going through the setup takes less than an hour. Honestly, going through the official MSW documentation takes less than an hour! 

If you’re working on a project that’s not too big, you can stop reading now, pat yourself on the back, and look at all those green check marks on your tests.

The advanced guide to Mock Service Worker

If you are working on a big application, maybe a robust dashboard, or an e-commerce website, you realize the basic setup presented above won’t cut it.

The handlers array will soon become a 10-thousand line mess. People may add mock servers in random tests, making debugging errors difficult. 

My team and I have faced those issues, and here’s how we decided to tackle them.

As stated above, the biggest issue in a big app using MSW is the handlers' files becoming unreadable. We tackled this issue by dividing the handlers’ files into separate, feature-related files. 

Now, instead of a single, long handlers.js we had something in the shape of the following structure:

src
│
└───tools
| └───jest
│ │ commonMswHandlers.js
│ │ server.js
| | setupTests.js
│
└───featureA
│ │ FeatureA.js
| |
| └───__tests__
│ │ FeatureA.test.js
│ │
│ └───msw
│ │ mswHandlers.js
│
└───featureB
│ │ FeatureB.js
| |
| └───__tests__
│ │ FeatureB.test.js
│ │
│ └───msw
│ │ mswHandlers.js
view raw msw-test.5.tsx hosted with ❤ by GitHub

A few API endpoints which were commonly needed for the tests and did not clearly fit any feature were left in the commonMswHandlers.js file.

Once we’ve effectively sliced the handlers array, we started wondering how these specific request handlers should be consumed. As we saw above, we could let the test authors import setupServer with specific handlers directly in the test files.

However, this quickly became quite verbose. We decided to create helper functions in our mswHandlers.js files. An example file would look like this:

// src/FancyHomepageFeature/msw/mswHandlers.js
import { rest } from 'msw';
// import the server created for the entire test suite
// this mock server includes commonMswHandlers
import { server } from '../tools/jest/server';
const homeHandlers = [
rest.get('https://fancy-app.com/homepage-data', (req, res, ctx) => {
return res(
ctx.json({
contents: [
{
banner: 'Fancy Banner'
},
],
})
);
}),
];
export const setupHomeHandlers = () => {
server.use(...homeHandlers);
};
view raw msw.test.6.tsx hosted with ❤ by GitHub

As you can see in the code above, we’re using the server.use() function. This function prepends request handlers to the current server instance. This means we are using everything that has been set up in the tools/jest/server.js file, and we’re adding a specific request handler for a specific URL.

Now, to use this helper function in a test, the test author would need to write something like this:

// src/FancyHomepageFeature/__tests__/FancyHomePage.test.js
import { FancyHomePage } from "../FancyHomePage";
import { setupHomeHandlers } from "./msw/mswHandlers";
describe("<FancyHomePage />", () => {
beforeEach(() => setupHomeHandlers());
test("displays homepage data", async () => {
// Render homepage with backend data
});
});
view raw msw.test.7.tsx hosted with ❤ by GitHub

The test author must import the server helper and run it in a beforeEach block.

Tests in this particular test suite will now look into the homeHandlers array when the components need something from the *https://fancy-app.com/homepage-data* endpoint, while other test suites remain unaffected by this change.

Testing errors

What if you would like to test that your website can handle network errors on specific endpoints? 

Let’s add another helper function to the FancyHomepageFeature/msw/mswHandlers.js file:

// src/FancyHomepageFeature/msw/mswHandlers.js
import { rest } from 'msw';
import { server } from '../tools/jest/server';
//...
export const setupFaultyHomeHandlers = () => {
server.use(
rest.get('https://fancy-app.com/homepage-data', (_req, res, ctx) => {
return res(ctx.status(200), ctx.json({weirdDataType: 'something went wrong'}));
}),
);
};
export const setupFailedHomeHandlers = () => {
server.use(
rest.get('https://fancy-app.com/homepage-data', (_req, res) => {
return res.networkError('Failed to connect');
})
);
};
view raw msw.test.8.tsx hosted with ❤ by GitHub

We can add tests for the scenarios described above: what should happen if the server returns a successful response, but the values are unexpected? How should the website behave if there is a network error? 

Let’s see those tests in action:

// src/FancyHomepageFeature/__tests__/FancyHomePage.test.js
import { FancyHomePage } from "../FancyHomePage";
import {
setupHomeHandlers,
setupFaultyHomeHandlers,
setupFailedHomeHandlers,
} from "./msw/mswHandlers";
describe("<FancyHomePage />", () => {
beforeEach(() => setupHomeHandlers());
test("displays homepage data", async () => {
// Render homepage with backend data
});
});
describe("<FancyHomePage /> - faulty data", () => {
beforeEach(() => setupFaultyHomeHandlers());
test("displays readable error when receiving unexpected data", async () => {
// Render homepage with error information
});
});
describe("<FancyHomePage /> - network error", () => {
beforeEach(() => setupFailedHomeHandlers());
test("displays network error", async () => {
// Render homepage with network error information
});
});
view raw msw.test.8.tsx hosted with ❤ by GitHub

Request handler overrides

If you read the MSW documentation carefully, you may be tempted to use the one-time override (res.once()) for testing errors. 

We have tried using this strategy in our codebase. We have found that using res.once() is counter-intuitive to the test author. If the test setup includes this one-time override, but the test suite has more than one test, only the first test will behave as expected. 

It may take the test author quite some time and great detective work to figure out that all his tests were correct, but the MSW handlers were prepared to handle only one request. If I was ever to use res.once(), it would only be directly in a test file, where it’s easily visible, and never in setup files.

Response object overrides

There is another type of override that I do recommend using - response object overrides. 

Let’s say you would like to test your network request with different, successful responses. You could write separate functions using server.use(), but that’s a lot of repetitive work. 

One of my colleagues, Jan Jaworski, came up with a very elegant solution to this problem. He created a helper function that could accept override data and use it in the response object thanks to the spread operator. 

The function looked roughly like this:

export const setupSmartMswHelper = (override = {}) => {
server.use(
rest.get("https://fancy-app.com/return-some-data", (req, res, ctx) => {
return res(
ctx.status(200),
ctx.json({
fancyData: "Im so fancy!",
moreData: "You already know",
...override,
})
);
})
);
};
view raw msw.test.9.tsx hosted with ❤ by GitHub

Here’s how this helper function would be used in a test:

// src/FancyHomepageFeature/__tests__/FancyHomePage.test.js
//...
test("handles updated data", async () => {
setupSmartMswHelper({
moreData: "Cant you taste this gold?",
});
//...
});
view raw msw.test.10.tsx hosted with ❤ by GitHub

This strategy is especially useful for big data objects we don’t want to re-type multiple times.

Testing errors - bonus content

Since we started testing network errors, we discovered the terminal output got polluted with many console.error messages. These error messages were expected, and we didn’t want them hiding real (as in UNexpected) errors.

We decided to add a console.error override in the Jest setup file:

// tools/jest/setupTests.js
const error = console.error;
console.error = (...args) =>
// Suppress error messages regarding network error in tests
/Error: Request failed with status code 500/m.test(
args[0]
)
? void 0
: error(...args);
view raw msw.test.11.tsx hosted with ❤ by GitHub

Thanks to this little function, we hid only the specific console errors we were sure about.

Summary

If you were to take away only one thing from this blog post, I sincerely hope it would be the notion that MSW is a great tool worth trying, even in a big project.

The solutions I described worked great in a real project. They may not be 100% ideal for your needs, but I hope you find some inspiration here to create the best, most comfortable, and developer-friendly test setup possible.

Insights

Learn more about Testing

How AI Agents Can Cut Manual QA by 30% — cover

Oct 30Article

How AI Agents Can Cut Manual QA by 30%

Explore an agentic QA workflow targeting 30% less manual effort across mobile, web, and TV, with test selection, failure triage, and validation.

Give QA Agents the Controls They Need With Appduct — cover

Oct 20Article

Give QA Agents the Controls They Need With Appduct

Use Appduct to give AI QA agents access to app functions for setting test conditions, triggering events, and checking results with UI automation.

cover

Oct 13Article

Agentic Mobile Testing With e2e and agent-device

See how e2e uses agent-device for AI-driven iOS and Android tests, combines exact assertions with agent actions, and replays cached steps without model calls.

Who Tests AI-Written Code? Inside TesterArmy With Szymon Rybczak — cover

Oct 12Podcast

Who Tests AI-Written Code? Inside TesterArmy With Szymon Rybczak

Szymon Rybczak explains how TesterArmy tests web and mobile apps with AI agents, creates test plans for pull requests, and fits into QA workflows.

Automating Mobile QA With Cloud Agents

Aug 4Article

Automating Mobile QA With Cloud Agents

Code review and green CI checks do not prove a mobile app still works. Learn how cloud agents, agent-device, and runtime evidence can verify app behavior before merge.

cover

Jul 21Webinar

Behavior Review for AI-Native Delivery

AI can increase code output faster than teams can verify the real app. See why behavior review matters before merge.

cover

Jul 6Article

Introducing Sniffler: Run only the E2E tests your changes can affect

Sniffler maps changed files to affected E2E tests, so teams can avoid running the full suite on every pull request.

cover

Jun 17Talk

Automating Mobile QA with Cloud Agents

Michał Pierzchała will present Automating Mobile QA with Cloud Agents at Ship London on 17 June 2026.

cover

May 13Podcast

How AI Is Changing React Native Development and Testing

Zack Ebenfeld joins React Universe On Air to talk about how AI is changing React Native workflows, testing, QA, and day-to-day engineering work.

cover

Apr 14Article

Agent React DevTools: Give AI Agents Access to React Internals

Agent React DevTools gives AI agents access to the React component tree, state, and performance data, enabling deeper debugging and more reliable QA for React and React Native apps.

cover

Mar 19Live Stream

Agent Device, AI workflows, and mobile testing in practice

A discussion on Agent Device, AI-driven workflows, and testing mobile apps using simulators and accessibility data.

cover

Aug 23Podcast

Introducing E2E Tests to React Native Core

This episode of the React Native Show dives into the recent introduction of End-to-End (E2E) testing in RNTester. Join us as we unpack the thought process, technical considerations, and practical implementation of this new approach to testing in React Native.

cover

Mar 19Tutorial

The Hard Way vs. The React Native AI SDK Way: Stop Writing Custom Modules for Every Model

Learn how React Native AI uses a unified provider model to switch between cloud and on-device LLMs without rewriting application logic, enabling reliable AI experiences online and offline.

cover

Mar 12Tutorial

Build Smarter Apps: Tool Calling & AI Orchestration Explained

Learn how on-device models in React Native can call tools, interact with external APIs, and return structured outputs you can use directly in application logic.

cover

Mar 5Tutorial

MLC LLM + React Native: On-Device AI Without the Pain

Learn how to run third-party on-device LLMs in React Native using MLC LLM. Choose your models, run them efficiently across platforms, and keep a unified JavaScript API with React Native AI.

cover

Feb 24Tutorial

Intelligent Fallbacks with Apple Intelligence in React Native

Learn how to use Apple’s built-in Foundation Models in React Native apps, reduce memory usage, and enable fast, fully on-device AI with React Native AI Apple.

cover

Feb 10Tutorial

What Is the React Native AI SDK? A Complete Intro & Quickstart

Learn why on-device AI matters for React Native apps, how local LLMs behave offline, and what problems React Native AI is designed to solve from day one.

cover

Jan 29Tutorial

Implementing an Android TurboModule with Kotlin

Learn how to implement a React Native TurboModule on Android, from the TypeScript spec and Codegen to the Kotlin implementation and package registration.

cover

Jan 19Tutorial

Working with Different Threads in Swift TurboModules

Learn how JavaScript, native module, background, and main threads interact in React Native TurboModules, and how to use GCD queues to run async work safely in Swift.

cover

Jan 9Tutorial

How to Add Type-Safe Constants to Swift TurboModules

Learn how to expose typed, immutable constants from a Swift TurboModule using Codegen, and get full end-to-end type safety between native and JavaScript.

cover

Dec 30Tutorial

Adding Event Emitters to Your TurboModule in Swift

Learn how to emit native events from a Swift TurboModule and subscribe to them in JavaScript, including proper memory management and Codegen integration.

Writing a Swift TurboModule in React Native

Dec 22Tutorial

Writing Your First TurboModule in Swift

Learn how to build a fully working TurboModule in Swift, integrate it with Codegen, and bridge it through Objective‑C inside an Expo app.