DEV Community

Cover image for Mock Service Worker (MSW) in Next.js – A Guide for API Mocking and Testing
Mehak.
Mehak.

Posted on

Mock Service Worker (MSW) in Next.js – A Guide for API Mocking and Testing

What is MSW?

MSW (Mock Service Worker) is a framework-agnostic library that intercepts network requests at the network level, providing mock responses. This means it doesn’t care whether you use fetch, Axios, GraphQL, etc.—MSW works regardless.

It achieves this using:

  • A Service Worker in the browser
  • A Node-based interceptor during testing

This ensures the mock lives closer to the actual request, mimicking real-world behavior without patching individual libraries.


How Does MSW Work?

MSW intercepts API calls at the network layer. This means:
If a mock exists for an API endpoint, MSW will return the mocked response.

If no mock is defined, the request proceeds to the real API.

Environment MSW Mode How it Intercepts
Browser Service Worker Runs a real service worker that intercepts requests from the browser in network layer
Node.js Node Interceptor Uses node-request-interceptor to hook into HTTP/HTTPS in process level

Why MSW ??

True Isolation
Mocks network at lowest possible level - no need to modify application code

Framework Agnostic Tests
Works with React Testing Library, Vue Test Utils, Svelte Testing Library, etc.

Realistic Behavior
Simulates actual network conditions (latency, errors, headers) unlike jest.mock()

Cross-Library Support
Intercepts requests from ANY HTTP client automatically


MSW in the Browser

To set up MSW in the browser for development:

  • Install the Service Worker

Run the following command to install the service worker file:

   npx msw init public/
Enter fullscreen mode Exit fullscreen mode

This will add the mockServiceWorker.js file to your public/ directory.

  • Create your mock handlers Create a handler file with your REST or GraphQL mocks:
import {
  http, HttpResponse,
} from "msw";

export const handlers = [
  http.get(`/api/whoami`, () => {
    return HttpResponse.json({
      status: 200,
      data: {
         user: "Jack Sparrow"
      },
    });
  }),
]
Enter fullscreen mode Exit fullscreen mode
  • Set up broswer worker Create a browser.ts file to initialize the MSW browser worker:
import { setupWorker } from 'msw';
import { handlers } from './handlers'
export const worker = setupWorker(...handlers);
Enter fullscreen mode Exit fullscreen mode

I also created a initMock file to start initiate the mock worker

  • Start the worker Instead of starting the worker in Rootlayout I prefer to do it by creating a Wrapper component.
'use client';

import { useEffect, useState } from 'react';

export function StartMockWorker({children}: {children: React.ReactNode}) {
  const [isMockReady, setMockReady] = useState(false);

  useEffect(() => {
    async function enableMocks() {
      if (process.env.NODE_ENV === 'development') {
        const { initMocks } = await import('@/common/mock/initmock');
        await initMocks();
      }
      setMockReady(true);
    }

    enableMocks();
  }, []);

  if (!isMockReady) {
    return <div>Loading mocks...</div>;
  }

  return <>{children}</> ;
}

Enter fullscreen mode Exit fullscreen mode

MSW with Jest for Testing

Jest runs tests in a Node.js environment, not a real browser. This means:
No Browser APIs Exist
Service Workers (which power MSW in browsers) are a browser-exclusive feature - they don't exist in Node.js.

JSDOM Isn't a Real Browser
While Jest uses JSDOM to simulate a browser, it's still just a Node.js process. Critical browser features like Service Workers aren't implemented.

Setup

  1. Create a server
import { setupServer } from 'msw/node'
import { handlers } from './handlers'
export const server = setupServer(...handlers)
Enter fullscreen mode Exit fullscreen mode
  1. In the jest setup file
// jest.setup.ts
import '@testing-library/jest-dom';
import { server } from '@/common/mock/server';
beforeAll(() => server.listen());
afterEach(() => server.resetHandlers());
afterAll(() => server.close());
Enter fullscreen mode Exit fullscreen mode

It ensures that your tests run with network requests intercepted and mocked, and that the state is cleaned up between tests.

Polyfills Required for Node + JSDOM

When running in jsdom with Jest, you might run into missing globals like:
Blob. TextEncoder,BroadcastChannel (added in MSW v2+), MessagePort etc

You’ll need to manually polyfill these in your Jest setup file


/* eslint-disable @typescript-eslint/no-require-imports */
// jest.polyfills.js


const {
  TextDecoder, TextEncoder,
} = require('node:util');

const { ReadableStream, TransformStream } = require('node:stream/web');

const { BroadcastChannel, MessagePort } = require("node:worker_threads")

Object.defineProperties(globalThis, {
  TextDecoder: { value: TextDecoder },
  TextEncoder: { value: TextEncoder },
  ReadableStream: { value: ReadableStream },
  TransformStream: { value: TransformStream },
  BroadcastChannel: { value: BroadcastChannel },
  MessagePort:{value:MessagePort}
})

const {
  Blob, File,

} = require('node:buffer')
const {
  fetch, Headers, FormData, Request, Response,

} = require('undici')

Object.assign(globalThis, {
  fetch,
  Headers,
  FormData,
  Request,
  Response,
  Blob,
  File,
})
Enter fullscreen mode Exit fullscreen mode

Conclusion

MSW is powerful, developer-friendly, and works seamlessly in both the browser and Node environments:

  • Great for mocking real-world APIs in local dev.
  • Essential for writing fast, reliable, and network-independent Jest tests.
  • Framework-agnostic and works with any HTTP client.

With proper setup and polyfills, it becomes your best friend in test and dev workflows.


Full Setup

You can find the complete setup, including all code examples and configurations, in this GitHub repo:
👉 msw-with-next

Top comments (2)

Collapse
 
itpretty profile image
itpretty

Finally get the SSE handler done in src/common/mock/handler.ts



// SSE (Server-Sent Events) handler
  http.get("/api/sse", () => {
    const stream = new ReadableStream({
      start(controller) {
        let counter = 0;

        const sendEvent = () => {
          const data = {
            id: counter,
            message: `Server message ${counter}`,
            timestamp: new Date().toISOString(),
            type: 'update'
          };

          const eventData = `data: ${JSON.stringify(data)}\n\n`;
          controller.enqueue(new TextEncoder().encode(eventData));

          counter++;

          if (counter < 10) {
            setTimeout(sendEvent, 1000); // Send event every second
          } else {
            // Send final event and close
            controller.enqueue(new TextEncoder().encode('data: {"type":"close","message":"Stream ended"}\n\n'));
            controller.close();
          }
        };

        // Send initial event
        sendEvent();
      }
    });

    return new HttpResponse(stream, {
      status: 200,
      headers: {
        'Content-Type': 'text/event-stream',
        'Cache-Control': 'no-cache',
        'Connection': 'keep-alive',
        'Access-Control-Allow-Origin': '*',
        'Access-Control-Allow-Headers': 'Cache-Control'
      }
    });
  })
Enter fullscreen mode Exit fullscreen mode
Collapse
 
itpretty profile image
itpretty

Many thanks for your detailed explanation on how to use MSW in Next.js

Based on your example I tried to mock SSE but fetch always failed.

MSW does not support mocking event stream?

// src/common/mock/handler.ts
http.get('/api/stream', () => {
    const stream = new ReadableStream({
      start(controller) {
        controller.enqueue(
            `event:some-event\ndata:some data\n\n`
        );
        controller.close();
      },
    });

    return new HttpResponse(stream, {
      headers: {
        "Content-Type": "text/event-stream",
      }
    });
  }), 
Enter fullscreen mode Exit fullscreen mode


// src/app/page.tsx
  useEffect(()=>{
    const sse = new EventSource('/api/stream');
    sse.onmessage = (ev) => {
        console.log(ev.data);
    };
  },[]);
Enter fullscreen mode Exit fullscreen mode