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 intercepted by worker patched via http module one source of truth for both environments

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.


← Back to Mock Service Worker (MSW) Setup