Loading...
There are a bunch of situations where mocking API requests can really come in handy. Maybe youโre...
msw/browser
)msw/node
) that works well for SSR and API routesnpm install msw --save-dev
mocks
directory at the root of your project. This will hold everything related to MSW, including handlers and setup files for both the browser and Node.js environments.
Hereโs the structure:
Letโs look at a simple example that fetches user information from the GitHub API. This will demonstrate both server-side and client-side fetching, which weโll later mock using MSW.. โโโ app โ โโโ page.tsx # Example route that makes an API call โโโ mocks โ โโโ browser.js # Client-side MSW setup (Service Worker) โ โโโ node.js # Server-side MSW setup (for SSR/API routes) โ โโโ handlers.js # Main list of request handlers โ โโโ data โ โโโ github-user.json # Mock json โโโ components โ โโโ MswWorker.tsx # Component to load the worker script โโโ instrumentation.ts # To integrate msw node server
app/page.tsx
GithubUserName
) and also displays the userโs GitHub login directly.
import GithubUserName from "@/components/UserName"; import { Fragment } from "react"; export default async function Home() { const res = await fetch("https://api.github.com/users/ajth-in"); const userData = await res.json(); return ( <Fragment> <GithubUserName /> <p className="login">@{userData.login}</p> </Fragment> ); }
components/UserName.tsx
useEffect
. Instead of showing the login, it displays the userโs full name.
"use client"; import { useEffect, useState } from "react"; export default function GithubUserName() { const [userData, setUserData] = useState<{ name: string } | null>(null); const [loading, setLoading] = useState(true); useEffect(() => { async function fetchUserData() { try { const res = await fetch("https://api.github.com/users/ajth-in"); const data = await res.json(); setUserData(data); } catch (error) { console.error("Failed to fetch user data:", error); } finally { setLoading(false); } } fetchUserData(); }, []); if (loading) return <p>Loading...</p>; if (!userData) return <p>Failed to load user data.</p>; return <p>{userData.name}</p>; }
๐ก Note:If you start the development server at this point, you'll see something like this:
The client-side fetching here is redundant. Since the same user data is already available on the server, it could be passed as a prop to the client component. We're doing it this way purely to demonstrate both client-side and server-side mocking with MSW.
Image showing live data
mocks/data/github-user.json
And add the following mock response:mocks/data/github-user.json
The "(cached)" part is just there to help us verify that the mock is being used when the app runs.{ "login": "ajth-in (cached)", "name": "Ajith Kumar P M (cached)" }
mocks/handlers.js
In this example, weโre intercepting all requests that match the patternimport { http, HttpResponse } from "msw"; import user from "./data/github-user.json"; export const handlers = [ http.get("https://api.github.com/users/*", ({}) => HttpResponse.json(user)), ];
https://api.github.com/users/*
. If you want to target a specific user or URL, you can provide the exact match instead.
mocks/node.js
import { setupServer } from "msw/node"; import { handlers } from "./handlers"; export const server = setupServer(...handlers);
mocks/browser.js
To hook MSW into our Next.js app, weโll use Next.js instrumentation hooks. These allow you to run setup code during the server's startup lifecycle.import { setupWorker } from "msw/browser"; import { handlers } from "./handlers"; export const worker = setupWorker(...handlers);
๐ ๏ธ From the Next.js documentation:Weโre going to use this hook to spin up our mock server.
"Instrumentation is the process of using code to integrate monitoring and logging tools into your application. This allows you to track the performance and behavior of your application, and to debug issues in production."
instrumentation.ts
instrumentation.ts
and add the following:
Hereโs what this does:export async function register() { if ( process.env.NEXT_PUBLIC_MSW_ENV === 'test' && process.env.NEXT_RUNTIME === 'nodejs' ) { const { server } = await import('./mocks/node'); server.listen(); } }
NEXT_RUNTIME === 'nodejs'
ensures we only run this in the Node.js runtime (not in the Edge runtime).NEXT_PUBLIC_MSW_ENV === 'test'
lets us control when to enable MSW. This way, mocks are only active in testing or development environments.โ ๏ธ Note:
This example uses Next.js version 15.3.5.
Depending on your version, you might need to enable the instrumentation hook explicitly in your config:
For client-side mocking, MSW uses a Service Worker to intercept requests in the browser. First, we need to generate the// next.config.ts export const nextConfig = { experimental: { instrumentationHook: true, }, };
mockServiceWorker.js
file that gets served from the public/
folder.
This will generate anpx msw init public/ --save
public/mockServiceWorker.js
file that MSW uses under the hood.
components/MswWorker.tsx
Basically, this waits until the mock worker is fully set up before rendering anything. That way, your app won't accidentally fire off real network requests before the mocks are ready. To make this work across your entire app, just wrap your layout with it."use client"; import { PropsWithChildren, useEffect, useState } from "react"; export function MswWorker({ children }: PropsWithChildren) { const [isMswReady, setIsMswReady] = useState(false); useEffect(() => { if (process.env.NEXT_PUBLIC_MSW_ENV !== "test") return; const enableMocking = async () => { const { worker } = await import("@/mocks/browser"); await worker.start({ onUnhandledRequest: "bypass", }); setIsMswReady(true); }; // @ts-expect-error msw not found if (!window.msw) { enableMocking(); } else { setIsMswReady(true); } }, []); if (process.env.NEXT_PUBLIC_MSW_ENV !== "test") return children; if (!isMswReady) { return "loading msw worker..."; } return <>{children}</>; }
layout.tsx
Now if you restart the dev server, you should see your app serving mocked responses right away.export default function RootLayout({ children, }: Readonly<{ children: React.ReactNode; }>) { return ( <html lang="en"> <body className={`${geistSans.variable} ${geistMono.variable}`}> <MswWorker>{children}</MswWorker> </body> </html> ); }
Screenshot showing a successful mocked response intercepted by MSW
instrumentation-client.ts
instrumentation.ts
for server-side setup, there's also an instrumentation-client.ts
file in Next.js that runs on the client before hydration starts.
In theory, we could load the MSW worker from there. Which sounds ideal, but after testing it out, I ran into some race conditions. Some API calls were firing before the worker was fully initialized, which kinda defeats the whole purpose of mocking.
At the time of writing this, clientInstrumentationHook
is still an experimental feature. So until it's more stable (or there's a solid workaround), itโs safer to stick with loading the worker manually in a client-side component like MswWorker.tsx
.
if (process.env.NEXT_PUBLIC_MSW_ENV === 'test') { import('./mocks/browser') .then(async mod => { if (mod?.worker?.start) { mod.worker.start(); } }) .catch(async () => { console.error("Failed to load the browser module"); }); }
โ ๏ธ Important:
This file (instrumentation-client.ts
) is executed before hydration, so avoid doing anything here that may block rendering or impact performance in production environments.
โ๏ธ Config Note:
Depending on your next js version, you might need to add the following tonext.config.ts
to enable this:
// next.config.ts export const nextConfig = { experimental: { instrumentationHook: true, clientInstrumentationHook: true, }, };