-
-
Notifications
You must be signed in to change notification settings - Fork 210
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
base: main
Are you sure you want to change the base?
Conversation
Server-side integrationI got the server-side MSW integration working in Next.js by using the 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
|
a17bcd1
to
06e8a00
Compare
* this module and runs it during the build | ||
* in Node.js. This makes "msw/browser" import to fail. | ||
*/ | ||
const { worker } = await import('../mocks/browser') |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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()
}
There was a problem hiding this comment.
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();
}
There was a problem hiding this comment.
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.
examples/with-next/next.config.mjs
Outdated
* export conditions and don't try to import "msw/browser" code | ||
* that's clearly marked as client-side only in the app. | ||
*/ | ||
if (isServer) { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
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:
`[MSW] Warning: intercepted a request without a matching request handler: |
Not checked it yet, We just set it up. |
'use client' | ||
import { useEffect, useState } from 'react' | ||
|
||
export function MockProvider({ |
There was a problem hiding this comment.
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 |
---|---|
There was a problem hiding this comment.
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.
@kettanaito I don't know why but MSW is not intercepting page request. The mock is enabled but does not catch any fetch. |
how does playwright work with this? My test makes the actual api call instead of the mocked call |
@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. |
await page.goto('/', { waitUntil: 'networkidle' }) | ||
|
||
const greeting = page.locator('#server-side-greeting') | ||
await expect(greeting).toHaveText('Hello, John!') |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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. :)
After using this example I've found that Next's partial prerendering feature doesn't work properly with the way |
I used this integration and so far it works fine. However I extended 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();
},
});
});
} |
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 |
Nevermind, just use JSON server. |
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... |
I am using NextJS 15.2.4 and am successfully mocking server-side API calls by starting 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 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 I can't see any pattern to this either. At first I thought maybe uncaught exceptions in our route handlers may be killing I have tried moving the code that starts I am at a loss to debug this but it is happening often enough that I am unsure if we can continue with EDIT: we are using TurboPack if that makes any difference? |
@robcaldecott I have also route handler mocking issue in Next.js |
OK, more info on #101 (comment) After doing some tests, it is definitely TurboPack causing |
@robcaldecott Thanks for sharing that. |
I experienced a similar issue. I could pinpoint it to an Digging deeper I was wondering, why Next.js wants to compile this file, because every time this request happened, it showed For me, the fix was to move this file to the |
Everything works fine on the client, but the path This is the related code I could find in the Next.js codebase: |
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: 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: // 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. |
I have the same problem running Next.js 15.4.3 and msw 2.10.4. Using |
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 |
Adds a Next.js 14 (App directory ) + MSW usage example.
Todos