mswjs/msw

Support Next.js 13 (App Router)

kettanaito opened this issue Β· 100 comments

Scope

Adds a new behavior

Compatibility

  • This is a breaking change

Feature description

As of 1.2.2, MSW does not support the newest addition to Next.js being the App directory in Next.js 13. I'd like to use this issue to track the implementation of this support since many people are currently blocked by this.

Prior investigation

I looked into this a month ago and discovered that there's difficulty to establish any process-wide logic in Next.js 13 due to the way Next itself is implemented internally. Very briefly, Next keeps two Node.js processes while it runs:

  1. One process is persistent (opened once on the same port and running there).
  2. Another process spawns at a random port, does its job, and gets killed.

This process separation is evident when you issue a hot reload to any of the layout components.

Reloading a root layout

When a hot update is issued through the root layout component, we can see the persistent Node.js process update. I don't remember any changes to the intermittent process.

Reloading a nested layout

When a hot update is issued for a nested layout, i.e. a certain page's layout, the intermittent process gets killed, then a new one spawns at its place (on a different port), likely evaluates the update, and keeps pending.

What's the problem with the two processes then?

In Node.js, MSW works by patching global Node modules, like http and https. Those patches must persist in the process in order for the mocking to work. Since Next uses this fluctuating process structure, it introduces two main issues:

  1. We cannot establish the global module patches once since Next's functionality is split across two different, unrelated processes. Usually, the module patching should be done somewhere in your root layout since it's conceptually guaranteed to be the highest hot update point no matter where you are in your application. That's not the case in Next.js, as the processes evaluating the root layout and individual page layouts are completely different, and the module patches in one process do not affect the other.
  2. Since the page-oriented (fluctuating) process constantly gets killed and spawned at random ports, I don't see a way to establish module patching in it at all to support API mocking for server-side requests issued in individual pages (or their layouts).

What happens next?

MSW is blocked by the way Next.js is implemented internally. I don't like this state of things but that's what we getβ€”patching Node globals is not the most reliable of things and it will, inevitably, differ across frameworks depending on how those frameworks are implemented.

I would pretty much like for the Next.js team to assist us in this. There's very little we can do on the MSW's side to resolve this. In fact, I believe there's nothing we can do until there's a way to establish a module patch in Node.js for all Next.js processes at the same time.

If you're blocked by this, reach out to the Next.js team on Twitter or other platforms, letting them know this issue is important. I hope that would help the team prioritize it and help us resolve it together. Thanks.

It's also worth mentioning that this is not an issue with MSW. The library works in an extremely straightforward way in any browser or Node.js process. This issue is here simply to track the progress of this Next.js integration as many developers have voiced their concerns and questions about it.

Does it need to have a persistent process for some reason or can it just repatch each time if there was a place to do it?

How did this work in Next.js Pages for API routes since _app.js doesn’t run for those?

A workaround is to just have a module that’s imported from every layout, page or custom route that uses data. Such as in a share api layer:


import β€œ./patch-msw”;

export async function getData() {
  return fetch(…);
}

We’re considering a global place to inject it too but not sure the process can be guaranteed to live on forever. That’s pretty limited.

@sebmarkbage, hi πŸ‘‹ Thank you for reaching out!

Does it need to have a persistent process for some reason or can it just repatch each time if there was a place to do it?

It can certainly re-patch on every hot update. We are doing precisely that for Remix and Svelte examples. I had trouble doing that with Next due to those dual processes running (a Node.js patch on the root layout doesn't apply to the server-side logic of individual pages since those seem to be evaluated in a different process).

How did this work in Next.js Pages for API routes since _app.js doesn’t run for those?

I suppose it was fine because the main use case is to support client- and server-side development of Next apps, which came down to:

  • Intercepting requests in the browser (that's handled by the service worker);
  • Intercepting server-side requests made in getServerSideProps (and similar), and these abode by _app.js so it was enough to patch Node modules there to enable API mocking.

Right now, the second one doesn't work due to the lack of _app.js alternative in Next 13.

A workaround is to just have a module that’s imported from every layout

I have two concerns regarding this workaround:

  1. This implies the imported code has some deduplication built-in so that the Node globals aren't patched by each individual import. We do have this logic present but it's still worth mentioning.
  2. This has negative DX implications since the developer is, effectively, forced to collocate API mocking with individual resource-fetching areas (i.e. a specific page route), which degrades overall experience when compared to the very same setups in other frameworks.

We’re considering a global place to inject it too but not sure the process can be guaranteed to live on forever. That’s pretty limited.

It would be really nice to have _app.js, or similarβ€”a designated place to establish client/server-side logic once and have it encapsulate all the future layouts/routes/etc. I understand this may be challenging based on Next's internals at the moment. If I can help somehow, just let me know.

We already have instrumentation.ts https://nextjs.org/docs/app/building-your-application/optimizing/instrumentation

There's an idea to expand that to include more features and to have different variants for the different module/scopes processes so that it can patch the module in the Server Components scoped, SSR scope and Client side scope.

Not sure if what is already there might be sufficient for your use case.

Thanks, @sebmarkbage. At first glance, it looks like it could work. I will give it a try in a new Next example repository and let you know.

@sebmarkbage, do you know if the instrumentation.ts hook supports ESM? It doesn't seem like it does:

// ./with-next/instrumentation.ts
export async function register() {
  // I've tried a regular top-level import, it matters not.
  const { server } = await import('./mocks/node')
  server.listen()
}
Module not found: Package path ./node is not exported from package /new-examples/examples/with-next/node_modules/msw (see exports field in /new-examples/examples/with-next/node_modules/msw/package.json)
> 1 | import { setupServer } from 'msw/node'
  2 | import { handlers } from './handlers'
  3 | 
  4 | export const server = setupServer(...handlers)

While with-next/node_modules/msw/package.json is:

{
  "exports": {
    "./node": {
      "browser": null,
      "types": "./lib/node/index.d.ts",
      "require": "./lib/node/index.js",
      "import": "./lib/node/index.mjs",
      "default": "./lib/node/index.mjs"
    },
  }
}

My first hunch was maybe the hook runs in the browser too, thus it's unable to resolve the exports['./node'].browser import field. But it doesn't seem to run in the browser.

This is really odd because MSW doesn't ship ESM exclusively, it comes as a dual CJS/ESM package, which means it has both the exports (for modern browsers) and root-level stubs like ./browser/package.json and ./node/package.json to support older ones.

You can reproduce this behavior in here: https://github.com/mswjs/examples-new/pull/7

What can be going wrong here?

I strongly suspect Next.js is trying to resolve the browser field when encountering Node.js export paths in instrumentation.ts for some reason. If I provide a dummy stub for exports['./node'].browser, it will pass the module resolution.

Moreover, it then fails on a bunch of other 4th-party imports (msw -> @mswjs/interceptors) that have Node.js-specific export fields. This doesn't seem right.

Do I have to mark the imported module in instrumentation.ts as Node.js-oriented so Next.js knows about it? Looks redundant, given instrumentation.ts is for server instance bootstrapping. It shouldn't even be reaching out to the browser exports field.

Hi πŸ‘‹

Firstly, thanks for the fantastic library.

I don't know if this is something that you're already aware of, but MSW doesn't appear to work with Next.js 13 full stop, not just with the app directory. It doesn't appear to work with the pages directory; the official example is also broken.

Is the issue with the pages directory encapsulated by this issue too?

Thanks πŸ˜„

Hi, @louis-young. Thanks for reaching out.

I'm aware of the library not working with Next 13 in general (I believe there are also issues on Next 12; I suspect there were some architectural changes merged to Next 12 prior to Next 13 which may have caused this).

However, to reduce an already complex issue, I'd like to track the App router exclusively here and approach it first. As I understand, the App router is the main direction the Next is going to take, and pages remain there mainly for backward compatibility reasons (the App router is far too big an endeavor not to be the main thing in 1-2 releases, eventually).

Thanks for getting back to me so quickly.

That's fair enough and a very reasonable and pragmatic approach, I just wanted to check that it was something that you were aware of.

Thanks again and keep up the great work πŸ˜„

@louis-young We have msw working with the pages directory just fine. Make sure you have this code at the top of your pages component's index.tsx:

if (process.env.NEXT_PUBLIC_API_MOCKING === 'enabled') {
  // eslint-disable-next-line global-require
  require('@/mocks');
}

You can create a client side only component that you include in your root layout.tsx.

"use client";
import { useEffect } from "react";

export const MSWComponent = () => {
  useEffect(() => {
    if (process.env.NEXT_PUBLIC_API_MOCKING === "enabled") {
      // eslint-disable-next-line global-require
      require("~/mocks");
    }
  }, []);

  return null;
};

edit: sorry I mistakenly assumed not working "full stop" meant client side too

@darrylblake, that would work but the main thing we're trying to solve here is the server-side integration.

Any news on this? Is there any ongoing task?

Any news on this? Is there any ongoing task?

And to add on, can we help at all? Anything you can point us to look at or do need help from the Vercel team?

Our team absolutely loves msw and we really want to use it on a green field we started last week

Module resolution in instrumentation.ts

The latest ongoing task is to figure out why instrumentation.ts in Next.js tries to resolve browser export fields when the module is meant for Node only: #1644 (comment).

If someone has the time to look into this, I'd be thankful. This import behavior doesn't seem right, and I don't believe it's related to MSW (we've tested out exports quite well).

Getting help from the Vercel team would be great since they know like no one else about the internals of their framework. But I understand and respect their limited availability, we're all doing our best effort here.

Minimal example

Even without the module resolution issue, what everybody can help with is the following:

  1. Create a new Next App directory project.
  2. Add the instrumentation.ts as specified in the docs.
  3. Simply print out the process id console.log(process.pid).
  4. Create a root layout and a page layout.
  5. Make a change to the root layout component (to trigger HMR). Does the console from instrumentation get printed to the terminal? If yes, remember the process id.
  6. Make a change to the page layout (to trigger HMR). Does the same message get printed? Are the process IDs the same?

If we can gather more details on how instrumentation.ts actually behaves across this split process structure of Next, we'd be unlocking new potential solutions to the problem at hand. Thanks!

Even without the module resolution issue, what everybody can help with is the following:

  1. Create a new Next App directory project.
  2. Add the instrumentation.ts as specified in the docs.
  3. Simply print out the process id console.log(process.pid).
  4. Create a root layout and a page layout.
  5. Make a change to the root layout component (to trigger HMR). Does the console from instrumentation get printed to the terminal? If yes, remember the process id.
  6. Make a change to the page layout (to trigger HMR). Does the same message get printed? Are the process IDs the same?

I tried this in an existing project opted into instrumentation and found that HMR changes didn't trigger the console log.

I saw one console log with pid 28076 after the "compiled client and server successfully in XXX ms" event in the Next console output.

I then two more console logs, both with pid 28089 after the "compiling /instrumentation (client and server)" event and after some of my app's routes had also compiled.

Finally I saw a console log with pid undefined after the "compiling /src/middleware (client and server)" event.

I made a bunch of layout.tsx and page.tsx changes, but none of them seemed to prompt a console log.

@kettanaito Next.js maintainer here, I did not verify your use case but what I think is going on there is that:

  • the instrumentation file gets compiled for the server, in which case it should look for the module/main field
  • but then it also gets compiled for the edge runtime, in which case the resolution order would be: edged, worker, browser, module, main (I think). So perhaps adding another field for edged would solve your error.

Hi, @feedthejim. Thanks for the info. I've tried logging process.env.NEXT_RUNTIME in the register and it always prints nodejs. I assume that the function only runs for Node.js in my case since I don't have an edge function set up and I'm loading a page in the browser.

I see webpack in the stack trace. Are there any flags I can use to debug what webpack is trying to do regarding module resolution here?

On instrumentation hook

@sebmarkbage, I don't think the instrumentation hook works in a way to implement Node.js module patching. An example below.

Consider the following root layout and a page component (both fetching data on the server because that's the point):

// app/layout.tsx
async function getRootData() {
  return fetch('https://api.example.com/root').catch(() => null)
}

export default async function RootLayout() {
  const data = await getRootData()
  return (
    <html lang="en">
      <body className={inter.className}>{children}</body>
    </html>
  )
}
// app/page.tsx
async function getData() {
  const response = await fetch('https://example.com')
  return response.text()
}

export default async function Home() {
  const data = await getData()

  return <p>{data}</p>
}

And the instrumentation.ts hook as follows:

export async function register() {
  console.log('[i] %s %d', process.env.NEXT_RUNTIME, process.pid)

  globalThis.fetch = new Proxy(globalThis.fetch, {
    apply(target, thisArg, args) {
      console.log('[i] fetch', args)
      return Reflect.apply(target, thisArg, args)
    },
  })
}

Expected behavior

The instrumentation hook run as a part of the Node.js server of Next and executes its register function as promised. In the function, we're patching global fetch, which is a very rough emulation of what MSW will do in order to intercept your data fetching requests in server components. Then, I expect both fetch() calls in layout.tsx and page.tsx to print a console log statement that we've added in the instrumentation hook.

Current behavior

Nothing gets printed. I suspected that patching fetch in particular is problematic because Next itself patches fetch and that probably happens after the instrumentation hook runs. Can you please confirm that?

I do mention fetch specifically because global variables do get shared between the hook and the server components (while they are on the same process, more on that below).

On a relevant note, I've checked how this dual Node.js process architecture is handled with the instrumentation hook, and it seems like this:

[i] nodejs 5800
[layout] nodejs 5842
[page] nodejs 5842
[layout] nodejs 5800
[page] nodejs 5800

i comes from the instrumentation hook; layout and page logs come from respective server components.

It seems that the hook shares at least 1 process with the layout and the page (5800, this is random) while each of those server components also have the second Node.js process that is not shared with the hook (5842, also random).

I don't have the insight to say what is that random port used for. If by any chance Next evaluates the components as a part of that port process, then I don't see how MSW or any other third-party can establish any server-side side effects to enable features like API mockingβ€”there's no shared process to apply those effects in.

Can we have something like entry.server.tsx in Remix?

Even without the module resolution issue, what everybody can help with is the following:

  1. Create a new Next App directory project.
  2. Add the instrumentation.ts as specified in the docs.
  3. Simply print out the process id console.log(process.pid).
  4. Create a root layout and a page layout.
  5. Make a change to the root layout component (to trigger HMR). Does the console from instrumentation get printed to the terminal? If yes, remember the process id.
  6. Make a change to the page layout (to trigger HMR). Does the same message get printed? Are the process IDs the same?

I tried this in an existing project opted into instrumentation and found that HMR changes didn't trigger the console log.

I saw one console log with pid 28076 after the "compiled client and server successfully in XXX ms" event in the Next console output.

I then two more console logs, both with pid 28089 after the "compiling /instrumentation (client and server)" event and after some of my app's routes had also compiled.

Finally I saw a console log with pid undefined after the "compiling /src/middleware (client and server)" event.

I made a bunch of layout.tsx and page.tsx changes, but none of them seemed to prompt a console log.

Exactly the same results - 13.4.9

Can we have something like entry.server.tsx in Remix?

https://remix.run/docs/en/main/file-conventions/entry.server

Should we start asking Vercel for a similar feature? (You know they don't want the folks over at Remix "one 'upping" them)

Does anyone created an issue on the NextJS repo?

Would love to have the link so I can +1

Jaesin commented

It seems for the dev server at least, https://github.com/vercel/next.js/blob/673107551c3466da6d68660b37198eee0a2c85f7/packages/next/src/server/dev/next-dev-server.ts#L1759 is restoring fetch to the un-patched original.

When running with yarn start, mocks come through for me using the with-msw example but upgraded to msw v1.2.2 except for the first page load. At this point the mock server hasn't started yet.

Update: This was running nextjs 13.4.9 in classic mode (as apposed to app mode) so maybe not as much help here.

Jaesin commented

In app mode (https://github.com/Jaesin/with-msw-app), I added some logging and patched node_modules/next/dist/server/dev/next-dev-server.js to see what it changes as far as the global fetch.

  1. [Process 1] Starts.
  2. [Process 2] next.config is loaded.
  3. [Process 2] next-dev-server.js starts and backs up global fetch.
  4. [Process 2] src/instrumentation.ts loads and registers mocks right away.
  5. [Process 1] next.config is loaded.
  6. Incoming request.
  7. [Process 3] next.config is loaded.
  8. [Process 3] next-dev-server.js backs up global fetch (fresh nodejs copy).
  9. [Process 3] src/instrumentation.ts loads and registers mocks right away.
  10. [Process 3] next-dev-server.js Restores fetch from the msw patched fetch to the backed up version (fresh nodejs copy)
  11. [Process 3] page.tsx Loads url and fails to get mocked data.
  12. HMR
  13. Incoming request.
  14. [Process 3] next-dev-server.js Restores fetch from nextjs Patched version to the backup (fresh nodejs copy).
  15. [Process 3] page.tsx Loads url and fails to get mocked data.
    ...

13-15 repeat for subsequent requests.

It looks to me that the third process that handles all of the requests is instantiating next-dev-server before it initializes instrumentation and that is causing the default node fetch object to be backed up for restore before every request is handled.

>>> node_modules/.bin/next dev
Main process id: 96616
- warn Port 3000 is in use, trying 3001 instead.
- ready started server on 0.0.0.0:3001, url: http://localhost:3001
- info Loaded env from .../with-msw-app/.env.development
[next.config] File loaded. Process id: 96617.
- warn You have enabled experimental feature (instrumentationHook) in next.config.js.
- warn Experimental features are not covered by semver, and may cause unexpected or broken application behavior. Use at your own risk.

[next-dev-server.js] Backing up Fetch. Process ID: 96617. global.fetch PropertyDescriptors:
{
  length: { value: 0, writable: false, enumerable: false, configurable: true },
  name: { value: '', writable: false, enumerable: false, configurable: true }
}
- event compiled client and server successfully in 221 ms (20 modules)
- wait compiling...
- wait compiling /instrumentation (client and server)...
- event compiled client and server successfully in 321 ms (64 modules)
[instrumentation] File loaded. Process id: 96617.
[instrumentation][register] Registering mocks. Process ID: ${process.pid}
[instrumentation][register] API mocking enabled, starting.
Mocks initialized
[next.config] File loaded. Process id: 96616.

### Incoming Request ###

- wait compiling /page (client and server)...
- event compiled client and server successfully in 980 ms (495 modules)
[next.config] File loaded. Process id: 96620.
[next-dev-server.js] Backing up Fetch. Process ID: 96620. global.fetch PropertyDescriptors:
{
  length: { value: 0, writable: false, enumerable: false, configurable: true },
  name: { value: '', writable: false, enumerable: false, configurable: true }
}
- wait compiling /instrumentation (client and server)...
- event compiled successfully in 120 ms (273 modules)
[instrumentation] File loaded. Process id: 96620.
[instrumentation][register] Registering mocks. Process ID: ${process.pid}
[instrumentation][register] API mocking enabled, starting.
Mocks initialized
[next-dev-server.js] Restoring Fetch. Process ID: 96620. global.fetch PropertyDescriptors:
{
  length: { value: 2, writable: false, enumerable: false, configurable: true },
  name: { value: '', writable: false, enumerable: false, configurable: true },
  prototype: { value: {}, writable: true, enumerable: false, configurable: false },
  [Symbol(isPatchedModule)]: {
    value: true,
    writable: false,
    enumerable: true,
    configurable: true
  }
}
[next-dev-server.js] New global.fetch PropertyDescriptors:
{
  length: { value: 0, writable: false, enumerable: false, configurable: true },
  name: { value: '', writable: false, enumerable: false, configurable: true }
}
[Home] Process ID: 96620
[Home] Failed to load mock data

-  β”Œ GET / 200 in 602ms
   β”‚
   └──── GET http://my.backend/book 404 in 127ms (cache: MISS)

### HMR ###

- wait compiling...
- event compiled successfully in 234 ms (306 modules)

### Incoming Request ###

[next-dev-server.js] Restoring Fetch. Process ID: 96620. global.fetch PropertyDescriptors:
{
  length: { value: 2, writable: false, enumerable: false, configurable: true },
  name: { value: '', writable: false, enumerable: false, configurable: true },
  __nextGetStaticStore: {
    value: [Function (anonymous)],
    writable: true,
    enumerable: true,
    configurable: true
  },
  __nextPatched: { value: true, writable: true, enumerable: true, configurable: true }
}
[next-dev-server.js] New global.fetch PropertyDescriptors:
{
  length: { value: 0, writable: false, enumerable: false, configurable: true },
  name: { value: '', writable: false, enumerable: false, configurable: true }
}
[Home] Process ID: 96620
[Home] Failed to load mock data

-  β”Œ GET / 200 in 187ms
   β”‚
   └──── GET http://my.backend/book 404 in 88ms (cache: MISS)

### Incoming Request ###

[next-dev-server.js] Restoring Fetch. Process ID: 96620. global.fetch PropertyDescriptors:
{
  length: { value: 2, writable: false, enumerable: false, configurable: true },
  name: { value: '', writable: false, enumerable: false, configurable: true },
  __nextGetStaticStore: {
    value: [Function (anonymous)],
    writable: true,
    enumerable: true,
    configurable: true
  },
  __nextPatched: { value: true, writable: true, enumerable: true, configurable: true }
}
[next-dev-server.js] New global.fetch PropertyDescriptors:
{
  length: { value: 0, writable: false, enumerable: false, configurable: true },
  name: { value: '', writable: false, enumerable: false, configurable: true }
}
[Home] Process ID: 96620
[Home] Failed to load mock data

-  β”Œ GET / 200 in 61ms
   β”‚
   └──── GET http://my.backend/book 404 in 8ms (cache: MISS)

Running in prod mode:

  1. [Process 1] Starts.
  2. [Process 1] next.config is loaded.
  3. [Process 2] next.config is loaded.
  4. [Process 2] src/instrumentation.ts loads and registers mocks right away.
  5. [Process 3] next.config is loaded.
  6. [Process 3] src/instrumentation.ts loads and registers mocks right away.
  7. Incoming request.
  8. [Process 3] page.tsx Loads url and mocks load successfully!
  9. Incoming request.
  10. [Process 3] page.tsx Loads url and mocks load successfully!
    ...
>>> node_modules/.bin/next start --port 3001
Main process id: 90293
[next.config] File loaded. Process id: 90293.
- ready started server on 0.0.0.0:3001, url: http://localhost:3001
- info Loaded env from .../with-msw-app/.env.production
[next.config] File loaded. Process id: 90294.
- warn You have enabled experimental feature (instrumentationHook) in next.config.js.
- warn Experimental features are not covered by semver, and may cause unexpected or broken application behavior. Use at your own risk.

[instrumentation] File loaded. Process id: 90294.
[instrumentation][register] Registering mocks. Process ID: ${process.pid}
[instrumentation][register] API mocking enabled, starting.
Mocks initialized
[next.config] File loaded. Process id: 90295.
[instrumentation] File loaded. Process id: 90295.
[instrumentation][register] Registering mocks. Process ID: ${process.pid}
[instrumentation][register] API mocking enabled, starting.
Mocks initialized
[Home] Process ID: 90295
mocked
[Home] Process ID: 90295
mocked

MSW v1.2.2
Next.js v13.4.9.

Thank you for doing all that research @Jaesin!

Thank you for the colossal research, @Jaesin πŸ‘

It seems to come down to Next.js meddling with the global fetch, restoring it to the version it snapshots before running all the subsequent logic, like instrumentation.ts. I know it comes as strange from a maintainer of a library that also meddles with the global fetch (alas, I wish this wasn't so) but I wish Next.js would respect the environment's fetch before trying to reset it to whichever version was at the moment the server recorded it.

This points to a much larger issue as no request interception tools would work in Next.js at the current state. All of them are patching request-issuing modules, fetch included. I hope there's an official next step from the Next.js team regarding this. I would love for MSW users and Next.js users to be happy.

I'm eager to use msw with Next.js again. But for those who need to mock in Next.js in the meantime, here's my approach: I'm using Next.js' api routes feature to mock. It's about the same amount of work as using msw. This pattern works well if you're not already using the api routes feature.

My approach:

  1. Configure api routes that have the same structure as the real APIs, but return hard-coded mock data
  2. Configure my app to point to a different base URL using an environment variable

I'm eager to use msw with Next.js again. But for those who need to mock in Next.js in the meantime, here's my approach: I'm using Next.js' api routes feature to mock. It's about the same amount of work as using msw. This pattern works well if you're not already using the api routes feature.

My approach:

  1. Configure api routes that have the same structure as the real APIs, but return hard-coded mock data
  2. Configure my app to point to a different base URL using an environment variable

This is a great approach! Only trouble I found was my Intel based mac couldn't keep up with compiling the API routes and page in dev (this may have been fixed with the recent performance improvements). We decided to go with an express server that returns the mock data

Would it be viable to get MSW instrumented now even if it only works in prod mode? Running preview (vs dev) for msw mocks is likely a liveable tradeoff for many people

Jaesin commented

Would it be viable to get MSW instrumented now even if it only works in prod mode? Running preview (vs dev) for msw mocks is likely a liveable tradeoff for many people

@madi-tracksuit This example is working for me with shared mocks between FE and BE in production mode without any modification to msw.

Would it be viable to get MSW instrumented now even if it only works in prod mode?

@madi-tracksuit, I think it's important to stress that the outcome of this discussion is to explore and find a suitable Next + MSW setup rather than change MSW. We don't ship logic specific to frameworks or tools. Instead, MSW works on a simple premise: it applies itself in any Node.js process or any browser process.

So, to your question, yes, if using the preview/prod mode is something your team is fine with, then you should absolutely do that. The instrumentation.ts approach seems to be the right one to take for those modes.

One to watch, although it looks like this would just be useful for browser testing I think, rather than local development etc: vercel/next.js#52520 (comment) πŸ‘€

Jaesin commented

For the time being, patching the NextJS dev server is working for me in dev mode. Still no issues with MSW on the server side in prod mode. MSW is working for me in the browser by adding a custom client component to app/layout.tsx as mentioned earlier.

patches/next+13.4.10.patch

diff --git a/node_modules/next/dist/server/dev/next-dev-server.js b/node_modules/next/dist/server/dev/next-dev-server.js
index 0d91a15..6b1940e 100644
--- a/node_modules/next/dist/server/dev/next-dev-server.js
+++ b/node_modules/next/dist/server/dev/next-dev-server.js
@@ -1233,7 +1233,13 @@ class DevServer extends _nextserver.default {
         return nextInvoke;
     }
     restorePatchedGlobals() {
-        global.fetch = this.originalFetch;
+        // The dev server starts before instrumentation registers causing issues
+        // with MSW. Restoring fetch is meant to prevent a memory leak but there
+        // is no need to restore fetch if it hasn't been patched by NextJS yet.
+        if (Object.getOwnPropertyDescriptor(global.fetch, '__nextPatched')) {
+            console.log('restorePatchedGlobals')
+            global.fetch = this.originalFetch;
+        }
     }
     async ensurePage(opts) {
         var _this_hotReloader;

Edit, typo in the patch.

@madi-tracksuit This example is working for me with shared mocks between FE and BE in production mode without any modification to msw.

@madi-tracksuit, I think it's important to stress that the outcome of this discussion is to explore and find a suitable Next + MSW setup rather than change MSW. We don't ship logic specific to frameworks or tools. Instead, MSW works on a simple premise: it applies itself in any Node.js process or any browser process.

Thanks! And yep sorry I wasn't very clear, I more meant if that approach is working well outside of dev server, and it sounds like it does.

Would it be worth updating the msw Nextjs template/docs with the instrumentation.ts pattern? Even though it has a pretty significant caveat it will at least work with Next 13

@sebmarkbage, hi πŸ‘‹ Thank you for reaching out!

How did this work in Next.js Pages for API routes since _app.js doesn’t run for those?

I suppose it was fine because the main use case is to support client- and server-side development of Next apps, which came down to:

  • Intercepting requests in the browser (that's handled by the service worker);
  • Intercepting server-side requests made in getServerSideProps (and similar), and these abode by _app.js so it was enough to patch Node modules there to enable API mocking.

Right now, the second one doesn't work due to the lack of _app.js alternative in Next 13.

Just to be clear, MSW can't intercept requests that happen inside pages/api routes, right?

In the end, how to make MSW work in server components, are there any examples?

In the end, how to make MSW work in server components, are there any examples?

We are waiting to see if Next can offer a way for tools like MSW to intercept requests, but at the moment it's not possible.

Π’ ΠΈΡ‚ΠΎΠ³Π΅ ΠΊΠ°ΠΊ Π·Π°ΡΡ‚Π°Π²ΠΈΡ‚ΡŒ Ρ€Π°Π±ΠΎΡ‚Π°Ρ‚ΡŒ MSW Π² сСрвСрных ΠΊΠΎΠΌΠΏΠΎΠ½Π΅Π½Ρ‚Π°Ρ…, Π΅ΡΡ‚ΡŒ ΠΏΡ€ΠΈΠΌΠ΅Ρ€Ρ‹?

ΠœΡ‹ ΠΆΠ΄Π΅ΠΌ, смоТСт Π»ΠΈ Next ΠΏΡ€Π΅Π΄Π»ΠΎΠΆΠΈΡ‚ΡŒ способ ΠΏΠ΅Ρ€Π΅Ρ…Π²Π°Ρ‚Π° запросов Ρ‚Π°ΠΊΠΈΠΌΠΈ инструмСнтами, ΠΊΠ°ΠΊ MSW, Π½ΠΎ Π½Π° Π΄Π°Π½Π½Ρ‹ΠΉ ΠΌΠΎΠΌΠ΅Π½Ρ‚ это Π½Π΅Π²ΠΎΠ·ΠΌΠΎΠΆΠ½ΠΎ.

Thanks for the answer. Do not tell me if there is an analogue that supports next.js v13?

@gmonitoring, given that Next 13 is rather new, and in its current architectural state doesn't allow for any API mocking solution to work (no means to introduce process-wide server-side side-effects), it's unlikely you find any alternatives that would work there. All they work through patching Node.js globals/standard modules, and there's no means to do that in Next as of now.

This example

For me it's failing while trying to fetch:

[Layout] Loaded file: Process ID: 85630
[Home] Loaded file: Process ID: 85630
[Layout] Loaded file: Process ID: 85621
[Home] Loaded file: Process ID: 85621
[Home] Home(): Process ID: 85621
[Home] Process ID: 85621
[Layout] Loaded file: Process ID: 85621
[Layout] Loaded file: Process ID: 85621
[Layout] Mocking enabled.
Mocks server listening
Mocks initialized
- error TypeError: fetch failed
- error TypeError: fetch failed
digest: "1991512964"

any solution to this issue?

As a status update, I've voiced my concerns and suggestions in a relevant discussion in the Next.js repository: vercel/next.js#53409 (comment). There hasn't been any response from the team so far. They seem to be focused on introducing a Playwright-based test runner and the ability to run the Next.js process under test but that doesn't relate nor solve this issue. Based on the team's availability, I believe that MSW 2.0 will happen without Next.js support.

Not an official response, but we're working on trying to remove the workers that I believe are causing you problems. Hopefully we'll have an update soon.

So looking at the next release from yesterday and it looks like they have an experimental test mode for playwright where they explicitly recommend msw. Am I being optimistic or does this imply they've done some work to support msw in SSR?

So looking at the next release from yesterday and it looks like they have an experimental test mode for playwright where they explicitly recommend msw. Am I being optimistic or does this imply they've done some work to support msw in SSR?

Kinda partially, but also not in the way that most people want to use MSW, or how you can use MSW with other frameworks. See vercel/next.js#53409 (comment) for more info :)

we don't have the secondary process the original issue post mentions anymore, so I'm curious if maybe that makes things easier for you folks now? I'm sorry I have not read the full thread/lack context on mswjs.

That's exciting news, @feedthejim! I'd have to retest the original use case and see if it indeed makes things any better. Thanks for improving Next.js.

Update

While having a single process will likely solve the client/server-side components, Next still patches global fetch after the instrumentation hook, so third-parties have no means to patch fetch still.

@feedthejim, any chance to reconsider this? I can imagine this isn't as easy as it sounds. I can also raise a more official proposal if that'd help.

Check the example

pnpm dev and take a look at the terminal:

# instrumentation.ts
[instrumentation] server.listen()... nodejs undefined
MOCKING ENABLED FOR: 59084
{
  fetch: [AsyncFunction (anonymous)] {
    # I'm patching fetch with a random property.
    foo: 'bar',
    [Symbol(isPatchedModule)]: true
  }
}

# page.tsx
getUser 59084 {
  # fetch is a completely different object once it gets
  # to the page.tsx. It's not patched by MSW so it cannot
  # affect this outgoing server-side request.
  fetch: [AsyncFunction (anonymous)] {
    __nextGetStaticStore: [Function (anonymous)],
    __nextPatched: true
  }
}

I'm working with a fully client-side rendered Next.js 13 app using app routing and noticed when setting the layout.js to client-side rendering with the "use client" definition at the top of the file MSW started overriding requests as expected in my pages.

I know this isn't useful for those using Next.js for server-side rendering but thought I'd mention it in case it helps someone.

We followed this answer and it partially works:
It works, but from time to time the fetching fails and returns undefined data.

Import trace for requested module:
./node_modules/.pnpm/node-fetch@2.7.0/node_modules/node-fetch/lib/index.js
./node_modules/.pnpm/msw@1.3.1_typescript@5.2.2/node_modules/msw/lib/index.js
./mocks/handlers.ts
./mocks/server.ts
./src/core/infrastructure/api.ts
./src/app/news/page.tsx
 β¨― src/app/news/page.tsx (20:16) @ categories
 β¨― TypeError: Cannot destructure property 'data' of '(intermediate value)' as it is undefined.
    at News (./src/app/news/page.tsx:27:19)
  18 |   searchParams: { [key: string]: string | string[] | undefined };
  19 | }) {
> 20 |   const { data: categories } = await makeApiRequest("categories", [
     |                ^
  21 |     "categories",
  22 |   ]);

Testing a json-server mock server we don't get this issue at any moment; so, yes, MSW is not yet compatible with NextJS 13.

To be entirely frank, nothing is compatible with Next.js 13 from patching-based API mocking solutions. With the way Next is orchestrated internally right now, to module patches will survive its bootstrapping. Just making this transparent so everybody's on the same page.

This is 100% true with global fetch. I believe the intention is to make fetch ubiquitous so that it works identically in server and client components in a push to accommodate RSC design patterns.

Instead of surviving the bootstrapping would it be possible to patch after Next builds/initializes? I've seen workarounds that have used api wrappers that looked for env variables to switch msw mocks on or off.

I understand it's not ideal and that MSW use cases extend beyond just testing, but it would benefit a large portion of the community that is looking for a reliable test solution in the meantime.

Instead of surviving the bootstrapping would it be possible to patch after Next builds/initializes?

That's really a question to the Next.js team I'm raising. As a third-party tool, MSW cannot influence the way Next runs without Next explicitly giving some API to do that. Yes, that would be the best solution and I hope that's what happens.

The solution on the Next's side may be something like a new hook in the instrumentation.js module:

// instrumentation.js

// This is what Next currently supports.
// This function is called BEFORE Next bootstraps
// the environment (patches global fetch).
export function register() {
}

// This function is called as the very last thing
// when bootstrapping the environment. 
export function registerAfterEnv() {
  patchGlobals()
}

I'm a bit sad to see Next pursuing a testing integration instead of allowing third-parties to patch request-issuing modules. Testing is just one part of where one may use API mocking tools, and I dare say it's as big a part as prototyping and debugging.

I also hate to be this guy but this whole story would not exist if Next didn't meddle with globals and achieved user convenience through proper abstraction. MSW meddles with globals because there's no other choice. There is a choice for Next, but I trust the team to know better.

I've seen workarounds that have used api wrappers that looked for env variables to switch msw mocks on or off.

I believe those workarounds imply initializing setupServer deeper than your application's root. That's quite incorrect and may lead to all sorts of unexpected behavior. Module patching should sit on the root to survive hot updates and affect the entire server-side of the application as one would expect. Use those workarounds to your own risk.

This is 100% true with global fetch. I believe the intention is to make fetch ubiquitous so that it works identically in server and client components in a push to accommodate RSC design patterns.

Instead of surviving the bootstrapping would it be possible to patch after Next builds/initializes? I've seen workarounds that have used api wrappers that looked for env variables to switch msw mocks on or off.

I understand it's not ideal and that MSW use cases extend beyond just testing, but it would benefit a large portion of the community that is looking for a reliable test solution in the meantime.

Those workarounds work terribly.

Update

I've opened an official proposal to support side effects after the environment bootstrap in Next.js: vercel/next.js#56446

Important

Please upvote that proposal to show that you need Next.js supporting MSW. Thank you.

A temporary compromise solution:

// eslint-disable-next-line import/no-extraneous-dependencies
import { createServer } from '@mswjs/http-middleware';
import { handlers } from './handlers';

const httpServer = createServer(...handlers);
httpServer.listen(9090);

A temporary compromise solution:

// eslint-disable-next-line import/no-extraneous-dependencies
import { createServer } from '@mswjs/http-middleware';
import { handlers } from './handlers';

const httpServer = createServer(...handlers);
httpServer.listen(9090);

In which file do you this modification?

A temporary compromise solution:

// eslint-disable-next-line import/no-extraneous-dependencies
import { createServer } from '@mswjs/http-middleware';
import { handlers } from './handlers';

const httpServer = createServer(...handlers);
httpServer.listen(9090);

do you have a complete working example of this? Curious if you're using the instrumentation hook discussed here in addition to the MSW middleware.

code: @SalahAdDin @rmiller61

mocks/http.ts

import { createMiddleware } from '@mswjs/http-middleware';
import express from 'express';
import cors from 'cors';
import { handlers } from './handlers';

const app = express();
const port = 9090;

app.use(cors({ origin: 'http://localhost:3000', optionsSuccessStatus: 200, credentials: true }));
app.use(express.json());
app.use(createMiddleware(...handlers));
app.listen(port, () => console.log(`Mock server is running on port: ${port}`));

package.json

{
  "scripts": {
    "dev": "next dev",
    "mock": "npx tsx ./mocks/http.ts",
  }
}

axios settings:

axios.defaults.baseURL = 'http://localhost:9090';

start mock server yarn mock, start dev server yarn dev

code: @SalahAdDin @rmiller61

mocks/http.ts

import { createMiddleware } from '@mswjs/http-middleware';
import express from 'express';
import cors from 'cors';
import { handlers } from './handlers';

const app = express();
const port = 9090;

app.use(cors({ origin: 'http://localhost:3000', optionsSuccessStatus: 200, credentials: true }));
app.use(express.json());
app.use(createMiddleware(...handlers));
app.listen(port, () => console.log(`Mock server is running on port: ${port}`));

package.json

{
  "scripts": {
    "dev": "next dev",
    "mock": "npx tsx ./mocks/http.ts",
  }
}

axios settings:

axios.defaults.baseURL = 'http://localhost:9090';

start mock server yarn mock, start dev server yarn dev

So we start it as an independent server, cool! As a workaround looks great.

Thanks for proposing this, @hdsuperman. As a temporary solution, using MSW in a standalone server may work. But it limits the experience quite significantly, making you change the source code to request different resources and having no support during tests. I hope we move forward with the Next.js proposal in the upcoming weeks so we have a proper support for MSW in Next.

@louis-young We have msw working with the pages directory just fine. Make sure you have this code at the top of your pages component's index.tsx:

if (process.env.NEXT_PUBLIC_API_MOCKING === 'enabled') {
  // eslint-disable-next-line global-require
  require('@/mocks');
}

Could you present the structure of your mocks/index.[js/ts] file?

Thanks for proposing this, @hdsuperman. As a temporary solution, using MSW in a standalone server may work. But it limits the experience quite significantly, making you change the source code to request different resources and having no support during tests. I hope we move forward with the Next.js proposal in the upcoming weeks so we have a proper support for MSW in Next.

Firstly, congratulations on the excellent work leading the MSW library.

Do we have any news about support for Next.js?

@xereda Artem followed up here. TL;DR it looks like Next is going to stop patching fetch and instead handle caching through its own cache/nostore primitives. So we're basically waiting for a stable release of those APIs in order to have MSW support Next's app router.

@xereda Artem followed up here. TL;DR it looks like Next is going to stop patching fetch and instead handle caching through its own cache/nostore primitives. So we're basically waiting for a stable release of those APIs in order to have MSW support Next's app router.

Thank you @rmiller61! I will follow this.

@xereda Artem followed up here. TL;DR it looks like Next is going to stop patching fetch and instead handle caching through its own cache/nostore primitives. So we're basically waiting for a stable release of those APIs in order to have MSW support Next's app router.

@rmiller61

In the example below, I don't use the App Router, but the error still occurs.

https://github.com/xereda/nextjs-msw-example/tree/main

@kettanaito any more update on this? I'd really love to use msw in my Next app (both for local dev and unit testing purposes), but I've been fighting with it for about a week. If not, what are you (or anyone else in this thread) using to mock api responses? I'd rather not resort to a barebones json-server but that's the best alternative I think I've found so far.

Hi, @hmacafee. No updates to far. As mentioned above, keep track of any changes in this discussion: vercel/next.js#56446.

Note that you can still use MSW client-side (in the browser) in Next.js. Nothing changed there. For server-side, we are waiting for Next.js to move away from patching (and restoring) fetch. Once they do, MSW should just workβ„’.

Meanwhile, some folks are using @mswjs/http-middleware to move MSW request handlers to a standalone server (see #1644 (comment)). While this will work, note that this approach negates the main benefits of MSW so it's not preferred.

@kettanaito based on this issue #1801 it seems like even the client-side msw/browser portion of MSW is difficult to implement in Next. Even when doing all of the following:

  • dynamically import worker following the enablMocking pattern from msw's docs
  • calling worker.start() inside of the useEffect of a use client component (the only place it is ever imported)
  • incorporating the webpack workaround recommended in #1801

I still hit a Module not found: Package path ./browser is not exported from package 'msw'.

I'm new to MSW and somewhat begrudgingly married to Next for the foreseeable future. If we ought to be able to work on the browser side even with vercel/next.js#56446 unresolved, could you point me in the right direction?

@kettanaito

I'm sorry, but I ended up getting lost with so much information. We believe in taking two steps back and reevaluating the problem.

What is the procedure for installing MSW in projects with Next.js 14? (And we can disregard the configuration via App Router)

@kettanaito

I'm sorry, but I ended up getting lost with so much information. We believe in taking two steps back and reevaluating the problem.

What is the procedure for installing MSW in projects with Next.js 14? (And we can disregard the configuration via App Router)

It is a big thread but that's because things are a bit complicated at the moment. Client side mocking can work but it may be a bit difficult to set up, React Server Component/server-side mocking won't work at all. This is caused by Next, not by MSW.

If you're already lost from reading this thread then wait till you also start reading the various issues/discussions on the Next.js GitHub project, plus Artem's Request for Comments too πŸ˜„

If you'd rather not dig into the details then the easiest thing is probably to wait for Vercel to make the changes that are necessary for MSW to work. Once that's happened then Vercel or MSW will be likely to make an example of using Next and MSW together that you can copy from.

This issue thread and vercel/next.js#56446 are the best places to watch for updates at the moment.

Note: For anyone experiencing difficulty getting the wrong export from msw or @mswjs/interceptors (ie. browser vs node), take a look at my comment here. It may help with some of the issues experienced on this thread.

But also, I haven't done extensive browser testing with it, so I'd love to see if it holds up!

Regarding the cannot resolve ./browser import, I'm frankly at a loss as to why that happens. The only situation when it would, is when you import msw/browser in a Node.js process. I suspect when Next.js builds your client code, it doesn't respect the browser/node conditions, and the third parties still resolve using the node/default condition.

I believe folks have suggested how to fix this in next.config.js in #1877 (comment).

Updates

I've shared my findings on the Next.js and MSW integration: https://twitter.com/kettanaito/status/1749496339556094316

Opened a pull request to add it as an example: mswjs/examples#101

TL;DR

It's not there quite yet. The first step has been done but Next.js remains incompatible with MSW due to the lack of means to, once again, run an arbitrary async code once for both server-side and client-side parts of Next.js.

For anybody curious, I've got a somewhat functioning example of Next.js 14 + MSW right here: mswjs/examples#101. If you read the tweet I posed above, you understand why it's not a full integration example I'm yet ready to recommend to everyone. But it can get you going. If you find something to help fix pending issues (see the pull request's comments), please let me know.

Hi @kettanaito – Just wanted to say that I love this package and have been using it for unit testing react components for the best part of three years. I'm currently working on a new project that uses Next 14 so have been eagerly following this discussion because it would be great to be able to use msw again.

In the mean time, I wondered if you had any suggestions for what the next best option might be for mocking network responses for unit tests? Our stack is vitest + react testing library (moved over from Cypress which is also having issues with Next 14) and we use react query for data fetching.

Playwright is supported to my knowledge and has build in network mocking. You might want to give that a try.

In the mean time, I wondered if you had any suggestions for what the next best option might be for mocking network responses for unit tests?

You don't run the entire app during unit testing, so you can choose tools like Vitest and React Testing Library + MSW to test individual units of your application. Highly recommend browsing through the Examples.

For E2E testing, you can use Playwright. Just bear in mind its mocking API wouldn't be able to intercept server-side requests your app makes (which makes sense; no E2E testing tools can do that). Once we land the end-to-end integration of Next + MSW, you'd be able to intercept and mock both server-side and client-side requests in E2E tests.

On my side, I chose to implement a small mock server in combination with Playwright. I can run the tests in local and in the CI with a dedicated build.
Technically, I redirected all my requests calls (server and browser) to a API route on my application and the mock server responds with the right mock.

@noreiller, a great strategy πŸ‘ You can also use MSW as a standalone HTTP server. May be useful if you consider using it "natively" in Next.js but cannot as of now. Look into https://github.com/mswjs/http-middleware.

@kettanaito Thanks. I took a look at https://github.com/mswjs/http-middleware before implementing it but I didn't want to have another server to handle. 😝

@noreiller, a great strategy πŸ‘ You can also use MSW as a standalone HTTP server. May be useful if you consider using it "natively" in Next.js but cannot as of now. Look into https://github.com/mswjs/http-middleware.

Great advise, thank you!

Thanks @kettanaito – I was under the impression that msw was having issues running in unit test environments because that is primarily what I've used the tool for, but of course, vitest / jest has no dependency on Next.js so I've given it a go this morning and it was really simple to set up.

Any news here?

To summarize my findings after trying out various methods, it appears that mocking through Express seems to be the most effective approach. (link)

I modified an example provided by kettanaito and tested it. I preferred not to alter webpack, so I revised it to check for the presence of the window object. This approach allows us to update data upon server load, and upon navigating to a different page and calling the API again, the data updates can be observed through mocking. Similarly, mocking works well on the client-side as well.

However, it's important to note that the worker and server do not share memory. If data is updated at server mount, the client cannot detect it, and vice versa. Therefore, if you only want to test the CSR part in Next.js without modifying webpack, checking for the window object seems like a good approach.(component link)

Based on the current observations, creating an Express server and integrating it with msw for mocking appears to be the best solution.

Has anyone else tried next's experimental proxy?

https://github.com/vercel/next.js/tree/canary/packages/next/src/experimental/testmode/playwright

I was able to get RSC fetches AND suspense streaming to work correctly with MSW with some fanangling, and for frontend mocking, I used the approach from https://github.com/valendres/playwright-msw where the service worker is installed into the browser by playwright itself, and not by a nextjs integration.

With the above approach, I could share the same set of handlers for RSC and CSR. It only works with playwright, but it allows to fully mock a nextjs application. It does need some custom code, the code from playwright-msw cannot be used directly, but it seems like a good alternative.

Has anyone else tried next's experimental proxy?

https://github.com/vercel/next.js/tree/canary/packages/next/src/experimental/testmode/playwright

I was able to get RSC fetches AND suspense streaming to work correctly with MSW with some fanangling, and for frontend mocking, I used the approach from https://github.com/valendres/playwright-msw where the service worker is installed into the browser by playwright itself, and not by a nextjs integration.

With the above approach, I could share the same set of handlers for RSC and CSR. It only works with playwright, but it allows to fully mock a nextjs application. It does need some custom code, the code from playwright-msw cannot be used directly, but it seems like a good alternative.

Did you have any example about it?

Nothing complete, it was just me playing around. If I ever come back to it, I will try to make an example repo, or possibly try to submit a PR to playwright-msw.

Nothing complete, it was just me playing around. If I ever come back to it, I will try to make an example repo, or possibly try to submit a PR to playwright-msw.

We had to use MWS as a stand-alone http server.

I have found a way to mock API requests without using msw/node. I share it in hopes that it will help people. It uses Route Handlers of App Router or API Routes of Pages Router.

This approach is very simple. First, define a handler that catches /api/* using Route Handlers or API Routes. Next, the request argument of the handler in Route Handlers or API Routes is processed by msw. This will cause msw to generate a response object. Finally, return the response object from the handler.

This approach does not conflict with monkey patching by Next.js for the fetch API because it does not use msw/node. In addition, because it does not use msw/browser, it avoids many of the problems caused by ServiceWorker.

@mizdra - how does that mock responses for your endpoints? Presumably you'd have to add/remove the real endpoints each time (Next, afaik, will only give you one response if you have such a setup - and I believe it prefers static routes to the catchall routes (i.e. the catchall is a fallback, not an override)).

It also seems like this would only work for routes defined in your application (e.g. in your example, myapp.com/api/my-endpoint), excluding the ability to fetch from e.g. https://jsonplaceholder.com/ or real external endpoints.
Of course you could wrap all external calls with internal route handlers (and perhaps sometimes this is useful for other reasons), but it does limit your usecase if I understand correctly?

Edit: As I tried to play with the example, I also couldn't get the fetch call to reload correctly (I think perhaps this relates to Next.js' caching - but using the second argument to Next's fetch and then {cache: 'no-store'} or similar caused MSW to be unable to handle the request using your approach).
If you update your handlers.ts file, does your page change?

Hi there
perhaps it's a trivial and it doesn't meet the requirements, but I managed to mock my v14 NextJS app using Axios instead of fetch. However, for production, the app will utilize fetch as recommended by Next.

@chrisb2244 Your understanding is correct. My approach cannnot mock external API requests.

However, if you configure your project to request http://localhost:3000/api/* when you want to mock with msw and request the external API when you don't, you can mock the external API in a pseudo way. Please refer to the example below. You can toggle msw mocking with NEXT_PUBLIC_MSW=1 pnpm run dev or NEXT_PUBLIC_MSW=0 pnpm run dev.

As I tried to play with the example, I also couldn't get the fetch call to reload correctly (I think perhaps this relates to Next.js' caching - but using the second argument to Next's fetch and then {cache: 'no-store'} or similar caused MSW to be unable to handle the request using your approach).
If you update your handlers.ts file, does your page change?

This is because I forgot to add export const fetchCache = "default-no-store"; to app/layout.tsx. I just added that and I think the problem has been resolved. When you edit handlers.ts, the content of the page should change as well.

how does that mock responses for your endpoints? Presumably you'd have to add/remove the real endpoints each time (Next, afaik, will only give you one response if you have such a setup - and I believe it prefers static routes to the catchall routes (i.e. the catchall is a fallback, not an override)).

I am sorry. I did not understand what you are saying. Could you please explain in more detail?

@mizdra - using your new example I can see the behaviour you describe.

This is because I forgot to add export const fetchCache = "default-no-store"; to app/layout.tsx. I just added that and I think the problem has been resolved. When you edit handlers.ts, the content of the page should change as well.

Now it reloads for me - I didn't know about this mechanism for setting a global default, thank you.

how does that mock responses for your endpoints? Presumably you'd have to add/remove the real endpoints each time (Next, afaik, will only give you one response if you have such a setup - and I believe it prefers static routes to the catchall routes (i.e. the catchall is a fallback, not an override)).

I am sorry. I did not understand what you are saying. Could you please explain in more detail?

I think you addressed this with the environment variable.
I meant that it was not possible to have for example both /api/my-endpoint/route.ts and /api/[...slug]/route.ts (as you defined)
and then have fetch('http://localhost:3000/api/my-endpoint') with variable mocking.
The ${API_ENDPOINT} you introduce would allow this behaviour if required.

I think if you wanted to do this, you could also pass headers on your requests (for example with Playwright, or similar) and then parse those to determine handling in /api/[...slug]/route.ts, but I don't know the performance cost here.
I also believe that your app will deploy with the test endpoints available, even if the ${API_ENDPOINT} is set to a different URL. This might be a problem, depending on the handling in the mocked endpoints (security, visibility, or just more deployed code).
(You can directly navigate to www.my-deployed-site.com/api/my-mocking-user-endpoint even if the external endpoint is configured via ${API_ENDPOINT} to use www.a-secure-external-service.com/verify-user, for example).

@chrisb2244

I meant that it was not possible to have for example both /api/my-endpoint/route.ts and /api/[...slug]/route.ts (as you defined)
and then have fetch('http://localhost:3000/api/my-endpoint') with variable mocking.
The ${API_ENDPOINT} you introduce would allow this behaviour if required.

That's right. You must take care to configure your API endpoints for mocking and any other API endpoints so that they do not conflict.

One of msw's philosophies is that you can mock without changing the application code at all. msw makes use of monkey patches and ServiceWorker to intercept requests in order to achieve this.

My approach avoids the problem of monkey patches in Next.js, but loses that philosophy. The user may have to rewrite the application code, such as #1644 (comment), to mock the request. This is an important tradeoff.

seems fetch on serverside is not working. finally i came up with a solution that think works. I created an interceptor for fetch and then inside that i put an if statement that says if env is development use axios otherwise use fetch. and inside your root layout you put :

if (process.env.NODE_ENV === "development") {
require("../mocks");
}

this is also my fetch interceptor :

export const fetchInstanse = async (
  input: string | URL | Request,
  init?: RequestInit | undefined
) => {
  let options = init;
  const session = await auth();
  const jwt = session?.user.accessToken;
  if (jwt) {
    options = {
      ...options,
      headers: { ...options?.headers, Authorization: `Bearer ${jwt}` }
    };
  }
  if (process.env.NODE_ENV == "development") {
    try {
      const res = await axios(
        input as string,
        options as AxiosRequestConfig<any>
      );
      return {
        json: () => res.data
      };
    } catch (error) {
      throw new Error("");
    }
  }
  const res = await fetch(input, options);
  if (res.ok) {
    const result = res;
    return result;
  } else {
    if (res.status == 401) {
      await signOut();
      return;
    }
    throw new Error(res.statusText);
  }
};

please ignore auth processes inside fetch instance. its specific for my application

seems fetch on serverside is not working. finally i came up with a solution that think works. I created an interceptor for fetch and then inside that i put an if statement that says if env is development use axios otherwise use fetch. and inside your root layout you put :

if (process.env.NODE_ENV === "development") {
require("../mocks");
}

this is also my fetch interceptor :

export const fetchInstanse = async (
  input: string | URL | Request,
  init?: RequestInit | undefined
) => {
  let options = init;
  const session = await auth();
  const jwt = session?.user.accessToken;
  if (jwt) {
    options = {
      ...options,
      headers: { ...options?.headers, Authorization: `Bearer ${jwt}` }
    };
  }
  if (process.env.NODE_ENV == "development") {
    try {
      const res = await axios(
        input as string,
        options as AxiosRequestConfig<any>
      );
      return {
        json: () => res.data
      };
    } catch (error) {
      throw new Error("");
    }
  }
  const res = await fetch(input, options);
  if (res.ok) {
    const result = res;
    return result;
  } else {
    if (res.status == 401) {
      await signOut();
      return;
    }
    throw new Error(res.statusText);
  }
};

please ignore auth processes inside fetch instance. its specific for my application

Shouldn't MSW intercept all those fetchings?

This has worked for me:

// MswProvider.tsx
"use client";

import { handlers } from "@/utils/msw/handlers";
import { useEffect, useState } from "react";

export default function MswProvider({ children }: { children: React.ReactNode }) {
  const [mocksReady, setMocksReady] = useState(false);
  const mocksEnabled = process.env.NEXT_PUBLIC_API_MOCKING === "true";

  useEffect(() => {
    if (mocksEnabled) {
      if (typeof window !== "undefined") {
        import("msw/browser").then((a) => {
          a.setupWorker(...handlers)
            .start()
            .then(() => setMocksReady(true));
        });
      }
    }
  }, []);

  return (!mocksEnabled || mocksReady) && children;
}

With the usage from layout.tsx:

  return (
    <html lang={locale}>
      <body className={inter.className}>
        <MswProvider>
              {children}
        </MswProvider>
      </body>
    </html>
  );

@kettanaito I made next-app-router-with-msw-vitest-playwright based on the Draft Example.

Can I add a page for Next.js App Router Integration to the MSW documentation, or open a PR in the Next.js repo to create a template with MSW pre-configured using npx create-next-app --example with-msw my-app?

The reason is that there are currently no official resources on integrating the Next.js App Router with MSW, leading to incomplete setup projects and blog posts.

If you have time, I'd appreciate it if you could check if there are any improvements needed in my repo setup!

Update

I am collaborating closely with @feedthejim to conclude the Next.js + MSW usage example. He has shared some vital improvements to the server-side instantiation, which enabled proper support for HMR. There are still some issues pending around HMR and Next tearing down any previously existing patches (addressing in vercel/next.js#68193), and also one client-side HMR issue.

This is where you stay up-to-date: mswjs/examples#101

Please do not design custom interceptors/request handling logic. MSW already covers you, just be patient while we are finalizing the integration. Thanks.