Skip to content

add next.js app router example #101

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 15 commits into
base: main
Choose a base branch
from
Draft

add next.js app router example #101

wants to merge 15 commits into from

Conversation

kettanaito
Copy link
Member

@kettanaito kettanaito commented Jan 22, 2024

@kettanaito
Copy link
Member Author

Server-side integration

I got the server-side MSW integration working in Next.js by using the instrumentation hook:

export async function register() {
  if (process.env.NEXT_RUNTIME === 'nodejs') {
    const { server } = await import('./mocks/node')
    server.listen()
  }
}

This allows MSW to intercept server-side requests Next.js makes.

Downsides

  1. Next seems to evaluate the instrumentation hook once. The import graph it creates will not update if you change mocks/handlers.ts because nothing but the instrumentation hook depends on that import. This means stale mocks until you re-run Next.js/force it re-evaluate the instrumentation hook.

@kettanaito kettanaito force-pushed the with-next branch 2 times, most recently from a17bcd1 to 06e8a00 Compare January 22, 2024 18:27
* this module and runs it during the build
* in Node.js. This makes "msw/browser" import to fail.
*/
const { worker } = await import('../mocks/browser')
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Next.js puts this dynamic import from the browser runtime to the Node.js build by moving it to the top of the module.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about fixing like this?

if (typeof window !== 'undefined') {
  const { worker } = await import('../mocks/browser')
  await worker.start()
}

Copy link

@brycefranzen brycefranzen Jul 10, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This didn't work for me. I had to do this instead:

if (process.env.NEXT_RUNTIME !== "nodejs") {
    const { worker } = await import("../mocks/browser");
    await worker.start();
  }

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@brycefranzen, that may actually work around webpack resolving this import in Node.js as well. I still think that's a bug though.

* export conditions and don't try to import "msw/browser" code
* that's clearly marked as client-side only in the app.
*/
if (isServer) {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a hack. I'm not sure why webpack has trouble resolving export conditions. I suspect this isn't webpack's fault. Next.js runs a pure client-side component in Node.js during SSR build, which results in webpack thinking those client-side imports must be resolved in Node.js.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@kettanaito I think the 'use client' directive is a bit of a misnomer. Components marked with that directive can still be SSR and are by default in Next unless you lazy load with ssr: false. Obviously anything in useEffect would only run on the client, so I'm not sure why the dynamic import you have in the other file is placed in a Node.js runtime. Let me know if I'm missing any context.

Having said that, I pulled this repository down and ran dev and build and both succeeded.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Got it, thanks for clarifying, @dbk91!

I suspect webpack extracts that import and puts it at the top of the module for whichever optimization. This is a bit odd since import() is a valid JavaScript API in the browser so it can certainly be client-side only.

I know this example succeeds. I've added tests to confirm that and they are passing. But I'm not looking for the first working thing. I'm looking for an integration that'd last and make sense for developers. This one, in its current state, doesn't, as it has a couple of fundamentals problems.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Understood, that makes sense! I totally missed your follow up messages in your original Tweet—I was expecting something non-functional and didn't realize there was extra work to get these tests passing.

Either way, I've been following this for quite some time and appreciate the work you've put into MSW and specifically this integration. My team was using it prior to upgrading to app router and we've sorely missed it, but that's on us for upgrading.

Copy link

@kanzure kanzure Mar 15, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know it's not a long-term solution, but `await eval("import('msw/node')") is an option to avoid nextjs trying to be clever.

@SalahAdDin
Copy link

SalahAdDin commented Jan 31, 2024

I'm our company we are using this example as a reference.

@mizamelo
Copy link

mizamelo commented Feb 9, 2024

@SalahAdDin

I'm our company we are using this example as a reference.

is it working? I've tried to use, but it's showing these messages below:

Internal error: TypeError: fetch failed

`[MSW] Warning: intercepted a request without a matching request handler:

• POST https://telemetry.nextjs.org/api/v1/record`

carloscuesta added a commit to carloscuesta/carloscuesta.me that referenced this pull request Feb 9, 2024
@SalahAdDin
Copy link

@SalahAdDin

I'm our company we are using this example as a reference.

Is it working? I've tried to use it, but it's showing these messages below:

Internal error: TypeError: fetch failed

`[MSW] Warning: intercepted a request without a matching request handler:

• POST https://telemetry.nextjs.org/api/v1/record`

Not checked it yet, We just set it up.

'use client'
import { useEffect, useState } from 'react'

export function MockProvider({
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about using suspense?

mockProvider.tsx

'use client'

let triggered = false

async function enableApiMocking() {
  const { worker } = await import('../mocks/browser')
  await worker.start()
}

export function MockProvider() {
  if (!triggered) {
    triggered = true
    throw enableApiMocking()
  }

  return null
}

layout.tsx

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

By doing so, we can avoid wrapping children in the mock provider client component.
But I am not sure if this is a good solution.

useEffect Suspense
ss2 ss

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The goal of this is to defer the rendering of the children until the service worker is activated. You are proposing keeping the state internally but I don't see it affecting {children}. So they will render, and if they make any HTTP requests, those will not be intercepted because the worker is not ready yet.

@SalahAdDin
Copy link

@kettanaito I don't know why but MSW is not intercepting page request. The mock is enabled but does not catch any fetch.

@pandeymangg
Copy link

how does playwright work with this? My test makes the actual api call instead of the mocked call

@kettanaito
Copy link
Member Author

@pandeymangg, there should be nothing specific to Playwright here. You enable MSW in your Next.js app, then navigate to it in a Playwright test and perform the actions you need.

import { MSWProvider } from './msw-provider'

if (process.env.NEXT_RUNTIME === 'nodejs') {
const { server } = require('../mocks/node')

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would this mean the mocks are bundled with the production code?
Does that have any undesirable side-effects?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@votemike, this likely needs tweaking by adding a new env variable like MOCKS_ENABLED and checking that one too.

Comment on lines +19 to +23
export function MSWProvider({
children,
}: Readonly<{
children: React.ReactNode
}>) {
Copy link

@sebws sebws Nov 1, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
export function MSWProvider({
children,
}: Readonly<{
children: React.ReactNode
}>) {
export default ({
children,
}: Readonly<{
children: React.ReactNode
}>) => {

@kettanaito

Some success 😄

The reason the interval is cleared in the Svelte example is because this listener for beforeunload is called.

In the Next example, this event listener is not called (presumably beforeunload doesn’t get called due to how Next.js handles page changes). Therefore, nothing clears the intervals and the workers are not garbage collected.

I'm not 100% sure how it works, since in Svelte beforeunload is happening but it doesn't seem like window.location.reload is being called. I did notice this meant that changing the handler used in the server rendered component doesn't cause it to be reloaded.

I believe Next.js doesn't perform a full page reload because the MSWProvider has a parent (layout.tsx) so a Fast Refresh is triggered. In a normal React setup, the changed file is at the root (pre-rendering) and has no parents, so a full page refresh is triggered.

We can take advantage of the fact that Fast Refresh use the casing of exports in a file to check if hot reload is possible. See this docs page https://nextjs.org/docs/messages/fast-refresh-reload

Exporting a component as an anonymous component breaks this, and so it always triggers a full reload. Therefore, we can make the change here to get HMR (via full page reload).

I think given this info, it's worth still thinking about module.hot.dispose() which does not force a full page reload. This mimics the behaviour of Svelte, including unfortunately that changing the handlers doesn't reflect in the server-rendered components (until manual reload).

In my eyes it depends upon if it's a bug or not a bug that beforeunload isn't being run in Next.js when there's HMR for the MSWProvider. If that isn't a bug, then there's no reason for setInterval to be cleared and so no reason for the extra worker to disappear. As a relevant note, Svelte mentions something along these lines in their HMR package. But at the moment I don't see a massive amount of difference between using beforeunload here to do this vs module.hot.dispose()

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tried both approaches, named and anonymous export, but both fails on:
[MSW] Warning: intercepted a request without a matching request handler:

In browser logs I clearly see that the worker has started.

Starting MSW worker
[MSW] Mocking enabled.

On server side the requests are mocked, but on client side all requests are just warning logged.

What I am missing? Using Next 14.2

My provider looks like this:

'use client';

import { Suspense, use } from 'react';

const mockingEnabledPromise =
  typeof window !== 'undefined' && process.env.NEXT_RUNTIME !== 'nodejs'
    ? import('../mocks/browser').then(async ({ worker }) => {
        console.log('Starting MSW worker');
        await worker.start({
          onUnhandledRequest(request, print) {
            if (request.url.includes('_next')) {
              return;
            }
            const excludedRoutes = [
              'cognito-idp.eu-west-1.amazonaws.com',
              'cognito-identity.eu-west-1.amazonaws.com',
              'google-analytics.com',
            ];
            const isExcluded = excludedRoutes.some((route) => {
              return request?.url?.includes(route);
            });
            if (isExcluded) {
              return;
            }
            print.warning();
          },
        });
      })
    : Promise.resolve();

export default ({
  children,
}: Readonly<{
  children: React.ReactNode;
}>) => {
  return (
    <Suspense fallback={null}>
      <MSWProviderWrapper>{children}</MSWProviderWrapper>
    </Suspense>
  );
};

function MSWProviderWrapper({
  children,
}: Readonly<{
  children: React.ReactNode;
}>) {
  use(mockingEnabledPromise);
  console.log('MSWProviderWrapper children', children);
  return children;
}

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@funes79 not sure this is relevant to this comment I'm afraid. seems like an issue with how you've set up the handlers.

@kettanaito did you see my comment above?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not sure what issues you are trying to solve here.

The current state of this PR gives you client-side mocking. If something is not mocked, follow the Debugging runbook, the issue is likely elsewhere.

The only thing missing in the current state is a proper HMR support for client-side mocking.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are you responding to funes? My original comment is about client side HMR

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@sebws, yes, that is correct.

Thanks for diving deeper into the HMR issue.

Therefore, nothing clears the intervals and the workers are not garbage collected.

So you are saying that interval keeps the worker object (not the service worker, mind) persist between HMR? That still sounds a bit odd to me. Do you have proof that clearing that interval indeed solves the issue?

I don't see module.hot.dispose() as the way forward, to be frank. This is a low-level hackery that an average Next.js user shouldn't be exposed to. I don't want to ask developers to do that. The issue is clearly specific to how Next.js handlers client-side HMR, otherwise we had the same issue in other frameworks. This is just a mention that the proper fix for this one is likely on Next.js' side, and that's why we have vercel/next.js#69098.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

looking again, it looks like more generally it's about beforeunload being called and what that does, in particular deregistering the actual service worker.

      context.events.addListener(window, "beforeunload", () => {
        if (worker.state !== "redundant") {
          context.workerChannel.send("CLIENT_CLOSED");
        }
        window.clearInterval(context.keepAliveInterval);
      });

it's not just the interval which is keeping the worker object in memory. we can prove this by logging the creation of the interval, clearing it, and the issue persists.

I may have made the original post at 2am and I don't have a full picture of how msw works so some of this is best guess

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

for context on how I'm looking at this for if you or others are interested, I'm using the "Memory" tab of chrome devtools, and taking heap snapshots at various points. then in the filter, adding SetupWorkerApi and looking at the retainers.

I found this pretty confusing but key for me has been clicking through when there is a line of code referenced, as well as trying out what happens when you right click -> ignore this retainer.

@kettanaito kettanaito changed the title add next.js (app directory) example add next.js app router example Nov 13, 2024
await page.goto('/', { waitUntil: 'networkidle' })

const greeting = page.locator('#server-side-greeting')
await expect(greeting).toHaveText('Hello, John!')

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
await expect(greeting).toHaveText('Hello, John!')
await expect(greeting).toHaveText('Hello, Sarah!')

If the test is about checking the mocked data, it should match what's in the handler, right? Same for the other test. :)

@AllenWilliamson
Copy link

After using this example I've found that Next's partial prerendering feature doesn't work properly with the way MSWProviderWrapper is wrapped in Suspense.

@muhammad-saleh
Copy link

I've followed this PR to integrate msw into Next.js 15 app. I found that msw is bundled with the production code even if I added a flag and checked everywhere to not load msw unless this flag equals to enabled.
I'm checking specifically for some hard coded strings in the tough-cookie package which is used by msw.

image

Basically I search for: "boavista" in the bundled JS in the browser when I do next build && next start

image

This is definitely something that I don't want to bundle for our users.
image

@llamerr
Copy link

llamerr commented Jan 12, 2025

I used this integration and so far it works fine. However I extended server.listen with similar ignore list as browser side, and added streaming ignore on top of it

if (process.env.NEXT_RUNTIME === 'nodejs') {
  import('@/mocks/node').then(async ({ server }) => {
    server.listen({
      onUnhandledRequest(request, print) {
        if (request.url.includes('_next') || request.url === 'http://localhost:8969/stream') {
          return;
        }
        print.warning();
      },
    });
  });
}

@Lisenish
Copy link

Lisenish commented Feb 6, 2025

Just a heads up that this approach doesn't work with any requests made within Next.js middleware (probalby we should add disclaimer to PR itself and comment in the example itself?)

I think it's probably challenging to make it work due to edge runtime limitation in middleware (but maybe it will be possible/easier in the future it should be possible to switch runtime to node soon vercel/next.js#75624)

@SalahAdDin
Copy link

Just a heads up that this approach doesn't work with any requests made within Next.js middleware (probalby we should add disclaimer to PR itself and comment in the example itself?)

I think it's probably challenging to make it work due to edge runtime limitation in middleware (but maybe it will be possible/easier in the future it should be possible to switch runtime to node soon vercel/next.js#75624)

Nevermind, just use JSON server.

@Vondry
Copy link

Vondry commented Apr 11, 2025

For anyone looking to setup mocking for client side API calls in Next.js.


Of course since the mswjs is loaded dynamically, then the Next.js app is most likely rendered before the service worker is loaded in to the browser... It might or might not be a problem, but would be nice if there would be a standard way how to suspense rendering of the app until service worker is ready to intercept requests

@SalahAdDin
Copy link

For anyone looking to setup mocking for client side API calls in Next.js.

Of course since the mswjs is loaded dynamically, then the Next.js app is most likely rendered before the service worker is loaded in to the browser... It might or might not be a problem, but would be nice if there would be a standard way how to suspense rendering of the app until service worker is ready to intercept requests

Waiting for an example...

@robcaldecott
Copy link

robcaldecott commented Apr 16, 2025

I am using NextJS 15.2.4 and am successfully mocking server-side API calls by starting msw in my root layout.tsx, which is a server-side React component. Currently I am doing something like this:

export default async function Layout(props: { children: React.ReactNode }) {
  if (
    process.env.NEXT_RUNTIME === "nodejs" &&
    process.env.MOCKS_ENABLED === "true"
  ) {
    const { server } = await import("@/msw/node");
    server.listen({ onUnhandledRequest: "error" });
  }

  ...
}

Where @/msw/node is where the handlers are configured.

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

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

The good news is it works great, the bad news is that making changes to our Next route handlers (that are calling mocked APIs) often - but not always - seems to stop msw from intercepting anything and API calls end up hitting the real API. Once this happens I have to kill the dev server and run npm run dev again for the handlers to work. Making even minor changes to a route handler, saving the file and reloading the app is often enough to break it.

I can't see any pattern to this either. At first I thought maybe uncaught exceptions in our route handlers may be killing msw but that does not appear to be the case. There is nothing in the console logs to indicate msw has stopped either. It just stops intercepting anything and the usual console warnings for unhandled requests no longer appear.

I have tried moving the code that starts msw to the module level of layout.tsx but it makes no difference.

I am at a loss to debug this but it is happening often enough that I am unsure if we can continue with mew for server-side mocking. If anyone has any ideas on something I could try, I would be very grateful.

EDIT: we are using TurboPack if that makes any difference?

@ryota-murakami
Copy link

@robcaldecott I have also route handler mocking issue in Next.js v14.2.28.
Could you tell me route hander path and msw handler definition?

@robcaldecott
Copy link

OK, more info on #101 (comment)

After doing some tests, it is definitely TurboPack causing msw to stop working. When I use npm run dev with webpack, it is absolutely rock solid: I can make changes to server side code and msw just keeps on trucking. This is using the RSC layout.tsx method.

@ryota-murakami
Copy link

@robcaldecott Thanks for sharing that.
I'll try your solution will worth I haven't use Turbopack though.

@gotshub
Copy link

gotshub commented Apr 23, 2025

OK, more info on #101 (comment)

After doing some tests, it is definitely TurboPack causing msw to stop working. When I use npm run dev with webpack, it is absolutely rock solid: I can make changes to server side code and msw just keeps on trucking. This is using the RSC layout.tsx method.

I experienced a similar issue. I could pinpoint it to an icon.svg in the app folder. As soon as a browser requested it (e.g. for the favicon), the msw mocks stopped working.

Digging deeper I was wondering, why Next.js wants to compile this file, because every time this request happened, it showed Compiled /icon.svg in the log. Some time ago I configured svgr/webpack as explained in the docs of Next.js. Interestingly, it seems that Turbopack is handling SVGs like a Javascript file. This would explain why it is compiling them. But I can only assume why this makes the mocks to stop working (maybe because it is not using the App Router layouts and routes and therefore re-initializes the loading of the app).

For me, the fix was to move this file to the /public folder, because Next.js excludes these files from the compiler. Now, everytime this file is requested, the console logs don't appear anymore and the issue is gone.

@Armadillidiid
Copy link

Everything works fine on the client, but the path __nextjs_original-stack-frame keeps getting called recursively and failing with 500 server error. In 10 seconds, about 100 requests are sent which is slowing down my PC.

This is the related code I could find in the Next.js codebase:
https://github.com/vercel/next.js/blob/9a1cd356dbafbfcf23d1b9ec05f772f766d05580/packages/next/src/client/components/react-dev-overlay/internal/helpers/stack-frame.ts#L34

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.