Skip to content

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.

@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

@oleksii-lukin
Copy link

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

@gbrrrl-no
Copy link

I have managed to make it work, but it's not that beautiful and it's a mix of past solutions I foun here

Client Mocking:
Here I used the new instrumentation-client.tsx from NextJS 15.3. There's no need to export a register function, so it's just this:

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

export const worker = setupWorker(...handlers)
worker.start({ onUnhandledRequest: 'bypass' })

Note I'm not putting any conditions based on environment, but it's a project specification, since it's a demo. If you're using MSW for development mocking only, you may need to add a conditional statement here.

Server Side Mocking:
It's a bit more complex, but not much. First you must disable turbopack. After that I essentially added the starting logic to my instrumentation.ts file.

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

The issue with adding to the instrumentation file is that it only runs once, when the server starts and, as soon as you need to refresh it, your handlers are flushed. That way, the hack I used was to restart the server in my root layout:

export default async function RootLayout({
  children,
}: Readonly<{
  children: React.ReactNode
}>) {
  if (process.env.NEXT_RUNTIME === 'nodejs') {
    const { server } = await import('../mock/node')
    server.listen({ onUnhandledRequest: 'bypass' })
  }

  ...
}

That way msw is started every time next needs to recompile to reflect changes you made in your project.

@wenhaoy-0428
Copy link

wenhaoy-0428 commented Aug 8, 2025

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

I have the same problem running Next.js 15.4.3 and msw 2.10.4. Using instrumentation-client.ts to enable browser integration of msw will cause errors in 500 server error from __nextjs_original-stack-frame and the api route. Removing the browser integration, the server side interception works fine, but adding the browser side interceptions always has such errors, and server interception breaks.

@jhroemer
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.

Experiencing the same. Was this on Next 14 or 15? Current project I'm testing with is on next 14, and the path to 15 is somewhat involved.

@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.

Experiencing the same. Was this on Next 14 or 15? Current project I'm testing with is on next 14, and the path to 15 is somewhat involved.

Version 15. We've given up on it now. For Playwright tests we are using testProxy instead. We're no longer using mocks in the app itself.

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.