How to Configure MSW for Next.js Apps

Next.js executes code in three distinct environments simultaneously — browser, Node.js server, and Edge runtime — and that split is exactly what causes MSW to fail silently or throw ReferenceError: window is not defined when you follow framework-agnostic setup instructions. This page resolves the configuration gaps specific to Next.js App Router (13+) and shows how to align Mock Service Worker handler registration with Next.js’s hybrid rendering lifecycle.

Why This Scenario Occurs

Standard MSW documentation targets single-environment apps. Next.js complicates things in three ways:

  1. Module evaluation at build time. import { setupWorker } from 'msw/browser' evaluates during the server bundle phase, hitting window before any browser context exists.
  2. Next.js fetch caching. The framework deduplicates and caches fetch calls at the framework level. A cached response returns before the request ever reaches the service worker, making handlers appear non-functional.
  3. App Router’s strict server/client boundary. Code with 'use client' runs in the browser; everything else runs in Node.js. MSW browser initialisation must live entirely in the client tree, or it breaks SSR hydration.

Understanding the request interception pattern — where the service worker sits between the browser’s fetch engine and the network — clarifies why these boundaries matter: the worker only exists once the browser has registered it, never during server-side rendering.

Solution

1. Install MSW and generate the worker script

npm install msw --save-dev
npx msw init public --save

Commit public/mockServiceWorker.js to version control. Do not regenerate it at build time — a stable committed file guarantees the same worker version across all developer machines and CI agents.

Verify the file is reachable:

curl -s http://localhost:3000/mockServiceWorker.js | head -1
# Expected: // Mock Service Worker (version X.Y.Z)

2. Define type-safe request handlers

Create src/mocks/handlers.ts. Keep all interceptors here so both the browser worker and the Node.js server share the same handler array.

import { http, HttpResponse } from 'msw';

export const handlers = [
  http.get('/api/v1/users', ({ request }) => {
    const url = new URL(request.url);
    const role = url.searchParams.get('role') ?? 'viewer';

    return HttpResponse.json(
      { id: 'usr_123', role, status: 'active' },
      { status: 200 }
    );
  }),

  http.post('/api/v1/sessions', async ({ request }) => {
    const body = await request.json() as { email: string };
    return HttpResponse.json(
      { token: 'mock-jwt-token', email: body.email },
      { status: 201 }
    );
  }),
];

Aligning resolver payloads with your OpenAPI schema connects directly to the response shaping techniques that prevent contract drift between mock and production data.

3. Create the browser worker (client-side only)

Create src/mocks/browser.ts:

import { setupWorker } from 'msw/browser';
import { handlers } from './handlers';

export const worker = setupWorker(...handlers);

Create src/mocks/server.ts for Node.js contexts (tests and SSR):

import { setupServer } from 'msw/node';
import { handlers } from './handlers';

export const server = setupServer(...handlers);

4. Build the environment-aware bootstrapper

Create src/mocks/index.ts. This is the single entry point that any Next.js component or test file calls:

export async function enableMocking(): Promise<void> {
  if (process.env.NODE_ENV !== 'development') return;

  if (typeof window === 'undefined') {
    // Node.js / SSR path — used in tests and dev instrumentation
    const { server } = await import('./server');
    server.listen({ onUnhandledRequest: 'warn' });
    return;
  }

  // Browser path — registers the service worker
  const { worker } = await import('./browser');
  await worker.start({
    onUnhandledRequest: 'warn',
    serviceWorker: {
      url: '/mockServiceWorker.js',
    },
  });
}

5. Wire the bootstrapper into Next.js App Router

Create a dedicated MockProvider client component so mock initialisation never contaminates the server component tree:

// src/components/MockProvider.tsx
'use client';

import { useEffect } from 'react';

export function MockProvider({ children }: { children: React.ReactNode }) {
  useEffect(() => {
    if (process.env.NODE_ENV === 'development') {
      import('../mocks').then(({ enableMocking }) => enableMocking());
    }
  }, []);

  return <>{children}</>;
}

Wrap your root layout with it:

// src/app/layout.tsx
import { MockProvider } from '@/components/MockProvider';

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en">
      <body>
        <MockProvider>{children}</MockProvider>
      </body>
    </html>
  );
}

The useEffect fires only after hydration, guaranteeing the service worker registers in the browser without blocking SSR output.

6. Disable Next.js fetch caching for mocked routes

Next.js caches fetch responses at the framework level — calls that hit the cache return before the service worker can intercept them. Override caching per-call on any route your handlers cover:

// Before: Next.js may return a cached response, bypassing MSW
const res = await fetch('/api/v1/users');

// After: forces a real network call that MSW can intercept
const res = await fetch('/api/v1/users', { cache: 'no-store' });

For a global override during development, add this to next.config.ts:

import type { NextConfig } from 'next';

const nextConfig: NextConfig = {
  ...(process.env.NODE_ENV === 'development' && {
    experimental: {
      fetchCacheKeyPrefix: 'msw-dev',
    },
  }),
};

export default nextConfig;

SVG: MSW in the Next.js Rendering Pipeline

The diagram below shows where the browser worker intercepts requests versus where the Node.js server handles SSR fetches — clarifying why two separate MSW setups are needed.

MSW request interception flow in Next.js Diagram showing the browser service worker intercepting client-side fetch calls, and the msw/node server intercepting server-side fetch calls during SSR, both sharing the same handlers array. BROWSER NODE.JS / SSR handlers.ts shared mock logic http.get('/api/...') HttpResponse.json() MockProvider 'use client' Service Worker msw/browser fetch('/api/v1/users') cache: 'no-store' instrumentation.ts register() hook setupServer() msw/node await fetch(url) Server Component / RSC the worker imports the same handlers intercepted by worker patched via http module one source of truth for both environments One framework, several runtimes Four Next.js contexts. Client components use the browser worker. Server components and route handlers use the Node setup started from instrumentation. Middleware runs on the edge runtime where neither works. Each row names the practical consequence. Context Setup Consequence client component setupWorker in the browser needs an awaited start before render server component setupServer via instrumentation separate state from the browser route handler setupServer via instrumentation same instance as server components middleware (edge) neither works mock upstream of it instead The last row surprises people: the edge runtime has neither a Service Worker nor Node's http module.

Verification

Open your browser DevTools Network tab after npm run dev loads. Filter by Fetch/XHR. Any request matched by a handler shows (mock) in the Initiator column. If you see a real network request reaching your backend instead, the service worker has not yet registered — wait for the console message [MSW] Mocking enabled before reloading.

Run this one-liner to confirm the worker script is served correctly:

curl -s http://localhost:3000/mockServiceWorker.js | grep -m1 "Mock Service Worker"
# Expected output: // Mock Service Worker (version X.Y.Z)

Gotchas and Edge Cases

  • Next.js Middleware conflicts. If middleware.ts rewrites or redirects /api/* paths, those rewrites execute on the Edge before the browser service worker can intercept them for same-origin requests. Add a bypass header: in middleware, check for request.headers.get('x-msw-bypass') and return NextResponse.next() when present.
  • App Router parallel routes and intercepting routes ((.)foo segments). These generate additional fetch calls that MSW may surface as unhandled. Run with onUnhandledRequest: 'warn' first to audit them, then add explicit passthrough handlers with http.get('/path', () => passthrough()) for internal Next.js machinery you do not want to mock.
  • Turbopack in Next.js 14+. Turbopack changes module evaluation order during HMR. If you see the [MSW] Mocking enabled message disappear after a hot reload, re-register the worker by calling worker.stop() then worker.start() inside a useEffect cleanup. The mock lifecycle management pattern of stop-then-start on module re-evaluation prevents stale handlers persisting across HMR cycles.

FAQ

Why does onUnhandledRequest: 'error' break my app in development?

Next.js issues internal fetch calls for font loading, image optimisation (/_next/image), and analytics. MSW has no handler for these. Switch to 'warn' locally and audit the console for routes you need to passthrough. Reserve 'error' for CI pipelines where the handler set is complete and you want the build to fail on missing coverage.

How do I reset handlers between Vitest or Jest tests?

Call server.resetHandlers() in an afterEach block and server.close() in afterAll. Add server.listen({ onUnhandledRequest: 'error' }) in beforeAll. This pattern aligns with the advanced MSW handler patterns for per-test overrides using server.use().

Does this configuration work with Next.js Pages Router?

Yes. The browser worker bootstrapper is identical. The difference is where you call enableMocking(): in Pages Router, wrap _app.tsx content inside a useEffect rather than using a separate MockProvider component. The msw/node setup for API route testing is unchanged.


Two runtimes, two stores Three consequences of the browser and server mocks being separate processes: a record created client-side is invisible to a server render, seeding must happen on both sides, and a hydration mismatch appears when the two disagree. Each names the practical resolution. A client-created record is invisible server-side separate module instances seed both from the same fixtures Seeding must happen twice once per runtime import the same generated JSON in both Hydration mismatches appear server and client rendered different data make the seed the single source of truth The third is the visible symptom of the first two, and it is usually blamed on React rather than on the mock.

← Back to Mock Service Worker (MSW) Setup