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:
- Module evaluation at build time.
import { setupWorker } from 'msw/browser'evaluates during the server bundle phase, hittingwindowbefore any browser context exists. - Next.js fetch caching. The framework deduplicates and caches
fetchcalls at the framework level. A cached response returns before the request ever reaches the service worker, making handlers appear non-functional. - 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.
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.tsrewrites 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 forrequest.headers.get('x-msw-bypass')and returnNextResponse.next()when present. - App Router parallel routes and intercepting routes (
(.)foosegments). These generate additional fetch calls that MSW may surface as unhandled. Run withonUnhandledRequest: 'warn'first to audit them, then add explicit passthrough handlers withhttp.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 enabledmessage disappear after a hot reload, re-register the worker by callingworker.stop()thenworker.start()inside auseEffectcleanup. 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.
Related
- Writing Custom MSW Response Resolvers — per-test handler overrides and stateful response sequences
- How to Intercept Fetch Requests in React — browser-level interception mechanics before adding Next.js complexity
- Best Practices for Dynamic Response Shaping — returning realistic, schema-conformant payloads from handlers
← Back to Mock Service Worker (MSW) Setup