Intercept API Requests with Mock Service Worker

Take full control over network behavior during prototyping, testing, and debugging with Mock Service Worker.

msw

A solid testing strategy is essential for building reliable applications, and data mocking plays a crucial role by simulating different scenarios and creating dependable tests to ensure our applications function as expected. Since lower environment data can be unpredictable, a tool like MSW provides a more flexible and reliable solution for API mocking.

MSW not only enables efficient testing of both happy paths and edge cases, ensuring a more reliable application, but also simplifies prototyping by allowing frontend development to move forward without waiting for a fully functional backend API.

In this article, I’ll guide you through setting up MSW v2 to intercept API calls using a mock server, both for quick prototyping and for unit testing with Jest and React Testing Library.

View Source Code

What is Mock Service Worker (MSW)?

Mock Service Worker (MSW) is an API mocking library for the browser and Node.js. It intercepts outgoing network requests, observes them, and responds with mock data, giving us full control over our application's behavior.

MSW is fully environment-, framework-, and tool-agnostic. It works seamlessly with all request clients by intercepting requests at the network level instead of patching fetch. This approach allows developers to customize network behavior on demand, treating API mocking as a standalone layer.


Installation

Install MSW as a dev dependency by running the following command in your project:

npm i -D msw@latest

Request Handlers

Let's kick things off by creating the Request Handlers. These handlers intercept requests and manage responses. They can return a mocked response, combine a real response with a mock, or even return nothing at all.

I will be working with a single HTTP request handler for a GET https://api.github.com/repos/facebook/react/issues request and respond to it with a mocked JSON response.

// mocks/handlers.ts import { http } from 'msw'; import { openIssuesMockApi } from './openIssues.mockApi'; // Must match the real URL (won't be called in tests) const MOCK_API = { FB_REACT_ISSUES: 'https://api.github.com/repos/facebook/react/issues', }; // Handlers for successful API responses export const handlers = [ // Intercept GET requests to the specified URL http.get(MOCK_API.FB_REACT_ISSUES, () => { // Return a mock response with the open issues data return new Response(JSON.stringify(openIssuesMockApi)); }), ]; // Handlers for failed API responses export const handlersFail = [ // Intercept GET requests to the specified URL http.get(MOCK_API.FB_REACT_ISSUES, () => { // Return a mock response with a 400 status and error message return new Response(null, { status: 400, statusText: 'Something went wrong!', }); }), ];

Read about structuring handlers.


Browser Integration

The browser configuration enables interception of API requests during development, accelerating our rapid prototyping process. This allows us to work independently from a backend API and utilize predictable mock data. Additionally, this approach can be integrated into tools like Cypress for end-to-end testing, further enhancing our testing and development workflow.

Generating the worker script

The MSW CLI provide a command to generate the mockServiceWorker.js worker script into you application’s public directory by running:

npx msw init public --save

Setup worker

The setupWorker() function sets up API mocking in the browser. Called without arguments, it returns a worker instance with no request handlers. Adding request handlers as arguments creates initial handlers that persist even when resetting others.

// mocks/browser.ts import { setupWorker } from 'msw/browser'; import { handlers } from './handlers'; export async function isMockServiceEnabled() { if (!process.env.MOCK_SERVICE_WORKER) { return; } const worker = setupWorker(...handlers); return worker.start(); }

In package.json, add a script to run the mock server on demand:

{ "scripts": { "dev:mock": "MOCK_SERVICE_WORKER=true npm run dev", }, }

I created a global variable MOCK_SERVICE_WORKER in my webpack configuration to enable the setup worker. You can implement a similar approach in your preferred bundler.

Application Entry Point

The Service Worker is an asynchronous operation, it’s a good idea to defer the rendering of your application until the registration Promise resolves.

// index.tsx (async function entry() { /* This ensures the mock service worker could only be enabled in development. To enable MSW, run: npm run dev:mock More details: https://mswjs.io/docs/integrations/browser */ if (process.env.NODE_ENV === 'development') { const { isMockServiceEnabled } = await import('../mocks/browser'); await isMockServiceEnabled(); } root.render( <React.StrictMode> <App /> </React.StrictMode> ); })();

Start the Mock Server

At this point we should be able to run the mock server by running: npm run dev:mock

To verify successful activation, open your browser's DevTools console. You should see the following verification message from MSW:

With this setup in place, we now have the flexibility to modify request handlers and test a variety of scenarios and edge cases in our application. Such as: simulate different API responses, error conditions, and data variations without relying on the actual backend API. By adjusting the mock responses in our handlers, we can thoroughly test how our application behaves under different circumstances, ensuring robustness and reliability across a wide range of potential situations.

View a response with the real API.


Jest and React Testing Library

During unit testing, it's crucial to have a scalable configuration that allows for seamless integration into our test suite. The mock data we use for writing these tests is as important as the tests themselves—without proper planning, it can easily get out of hand. MSW provides structure and a foundation to cover all edge cases.

I'll assume you already have Jest and React Testing Library set up in your environment. Let's focus solely on integrating MSW. Before diving into the configuration.

Make sure are using Node.js 18 or newer. I also recommend checking out the MSW integration with Testing Library for additional context.

Jest set up

Jest operates in a Node.js environment, which lacks browser-specific features like the fetch API and service workers that MSW uses in browsers. However, by integrating MSW with Node.js, we can simulate API requests in this environment, ensuring our tests accurately mirror our application's interactions with external services. The best part is that We can reuse the request handlers we created for the browser integration.

For simplicity, I'll present this configuration in a single file. However, in a production environment, it's best to organize it in a separate testingUtils file for improved reusability.

First, import the setupServer function from msw/node and invoke it with your request handlers as the argument.

// App.test.tsx import { render, waitFor } from '@testing-library/react'; import { setupServer } from 'msw/node'; import { handlers, handlersFail } from '../mocks/handlers'; // declare which API requests to mock const server = setupServer(...handlers); // establish API mocking before all tests. // it is a synchronous API so you don’t have to await it. beforeAll(() => server.listen()); // reset any request handlers that are declared as a part of our tests // (i.e. for testing one-time error scenarios) afterEach(() => server.resetHandlers()); // clean up once the tests are done afterAll(() => server.close());

This unit test ensures our application correctly renders all expected data in the proper sequence.

// App.test.tsx const issueIds = ['30786', '30782', '30772', '30771', '30694', '30759']; it('shows loader initially, then renders title and Open Issues after successful fetch', async () => { const { findByTestId, getByText, queryByTestId } = render(<App />); // Verify the loader is displayed initially const spinner = await findByTestId('spinner'); expect(spinner).toBeInTheDocument(); // Wait for the title and data to be displayed await waitFor(() => { // Check if the title "Open Issues" is displayed expect(getByText(/Open Issues/i)).toBeInTheDocument(); // Check if each issue ID is displayed issueIds.forEach(item => { expect(getByText(new RegExp(item, 'i'))).toBeInTheDocument(); }); }); // Ensure the loader is no longer in the document expect(queryByTestId('spinner')).not.toBeInTheDocument(); });

This unit test verifies the "unhappy path." We use server.use(...handlersFail) to prepend request handlers to the current server instance. The handlersFail returns a 400 error, allowing us to test the application's behavior when an API request fails. This ensures our error handling works as expected. View final tests.

// App.test.tsx const issueIds = ['30786', '30782', '30772', '30771', '30694', '30759']; it('shows loader initially, then shows "No open issues found" when fetch fails', async () => { server.use(...handlersFail); const { findByTestId, queryByText, queryByTestId, getByText } = render( <App /> ); // Verify the loader is displayed initially const spinner = await findByTestId('spinner'); expect(spinner).toBeInTheDocument(); // Wait for the error message to be displayed await waitFor(() => { // Check if the error message "No open issues found." is displayed expect(getByText(/No open issues found./i)).toBeInTheDocument(); // Ensure none of the issue IDs are displayed issueIds.forEach(item => { expect(queryByText(new RegExp(item, 'i'))).not.toBeInTheDocument(); }); }); // Ensure the loader is no longer in the document expect(queryByTestId('spinner')).not.toBeInTheDocument(); });

⚠️ If we run our tests at this stage, we'll encounter the following error:

Request/Response/TextEncoder is not defined (Jest)

This issue occurs because our environment lacks Node.js globals. Jest deliberately removes these globals and doesn't fully restore them, requiring us to manually add them back. To do this, you we can create a jest.polyfills file; more details on this solution can be found here. To tackle this issue with the Node globals, I opted for jest-fixed-jsdom. This library reinstates global APIs common to both environments, ensuring enhanced interoperability, stability, and consistent runtime behavior.

Install jest-fixed-jsdom:

npm i -D jest-fixed-jsdom

Jest Config

Add jest-fixed-jsdom to the testEvironment and set the set the testEnvironmentOptions.customExportCondition to [’’] . This will force JSDOM to use the default export condition when importing msw/node, resulting in correct imports.

// jest.config.ts module.exports = { // The test environment that will be used for testing testEnvironment: 'jest-fixed-jsdom', // Specify custom export conditions for the test environment testEnvironmentOptions: { customExportConditions: [''], }, };

Finally 🎉

At this stage, our unit tests should run smoothly without any hiccups.

npm run test


Conclusion

Integrating Mock Service Worker into our development workflow streamlines API mocking, testing, and prototyping. MSW allows us to intercept network requests, providing complete control over our app's behavior in various scenarios and ensuring comprehensive test coverage. Although the initial setup may appear challenging, MSW's flexibility and scalability make it a worthwhile investment—ultimately saving your team significant time and effort.

Recommended links: