Mock server-side requests in Playwright (Next.js, TanStack Start)
In a Next.js or TanStack Start app, the server calls your API while it renders a page. Playwright’s request mocking never sees those calls. The sections below explain why, then compare four ways to control them in a Playwright test: Next.js test mode, MSW inside the server, a forward proxy such as mockttp, and recording real responses with test-proxy-recorder. Facts about the other tools were checked against their own docs and source on 2026-10-03.
Why page.route() can’t see server-side requests
Section titled “Why page.route() can’t see server-side requests”Playwright’s page.route() works on “network requests that are made by a page”. A Server Component, a route loader or a server function runs in your app’s Node.js process instead. Its fetch goes from that process straight to the API. The request never passes through the browser, so Playwright has nothing to intercept. page.route() does see the page request the browser sends to your app, and the HTML that comes back. It never sees the requests the server made to build that HTML.
HAR files have the same limit, because page.routeFromHAR() serves “the network requests that are made in the page”.
MSW’s comparison with Playwright says the same: page.route() affects “the traffic in the spawned browser, not the Node.js process”. The Playwright team’s post on server-side mocking says page.route() works for browser requests, and “For server-side HTTP calls, it doesn’t.” The feature request for built-in server-side mocking, microsoft/playwright#30766, was closed on 2026-05-22 with a link to that post.
Next.js test mode
Section titled “Next.js test mode”Next.js ships an experimental Playwright integration at next/experimental/testmode/playwright. Its only documentation is a README in the Next.js repository, and the Next.js Playwright guide does not mention it. A test registers handlers with next.onFetch(), and Next.js sends the server’s fetch calls to them:
// Shortened from the READMEimport { test, expect } from 'next/experimental/testmode/playwright';
test('/product/shoe', async ({ page, next }) => { next.onFetch((request) => { if (request.url === 'http://my-db/product/shoe') { return new Response(JSON.stringify({ title: 'A shoe' }), { headers: { 'Content-Type': 'application/json' }, }); } return 'abort'; });
await page.goto('/product/shoe'); await expect(page.locator('body')).toHaveText(/Shoe/);});What it needs:
experimental: { testProxy: true }innext.config.js.- A
playwright.config.tsbuilt withdefineConfigfromnext/experimental/testmode/playwright, and tests that importtestfrom the same module.
Under the hood, each Playwright worker starts a small proxy server. The fixture adds a Next-Test-Proxy-Port header (that proxy) and a Next-Test-Data header (the test id) to the page’s requests (next-fixture.ts). With testProxy on, the server reads those headers and sends each server-side fetch to that proxy. The proxy hands the request to that test’s handlers (fetch.ts). Handlers are kept per test id, so parallel tests can share one Next.js server. A server fetch that no handler answers throws Proxy request aborted. A handler can return 'continue' to let a request through instead. next.onFetch() also receives the page’s own fetch calls to other origins.
Status:
- Experimental. The import path and the config key both say so, and the README is titled “Experimental test mode for Playwright”.
- It still gets fixes. The latest, vercel/next.js#96525, was merged on 2026-08-13.
- The README also offers MSW handlers per test through
next/experimental/testmode/playwright/msw. With MSW 3.0 (released 2026-09-28) that entry point fails to load. It importsstrict-event-emitter, which MSW 3.0 no longer depends on. It also calls MSW’shandleRequest(), which 3.0 removed (MSW 3.0 release notes,msw.ts). It loads with MSW 2.15.0. Both results are from Next.js 16.3.8, checked on 2026-10-03.
MSW in the Next.js server
Section titled “MSW in the Next.js server”MSW intercepts requests in Node.js with setupServer from msw/node (Node.js integration). Its docs have no Next.js guide. When MSW’s author closed the App Router issue, he named mswjs/examples#101 as the integration reference (comment). That pull request has been open since 2024-01-22. It starts the interceptor from the root layout, with a fixed list of handlers:
// app/layout.tsx, from mswjs/examples#101if (process.env.NEXT_RUNTIME === 'nodejs') { const { server } = require('@/mocks/node'); // setupServer(...handlers) server.listen();}The handlers then live in the Next.js process. The example starts that process with Playwright’s webServer, which runs one process for the whole run (Playwright team’s post). Every worker sends its pages to that one server, and parallel workers change how its handlers behave:
- A test can’t call
server.use()on that server, because the test runs in its own worker process. MSW has no released API for changing handlers in another process. The proposal,setupRemoteServerin mswjs/msw#1617, has been open since 2023-05-12. MSW’s Playwright binding says it relies onpage.route()until that ships (@msw/playwright). - If you add your own channel, such as an API route calling
server.use(), the change reaches every test. Handlers added withserver.use()“persist on the server instance”. The server boundary post explains that concurrent overrides on one server become “a global state shared between all tests”. In #1617, MSW’s author notes that in practice you either keep “a fixed list of handlers” or start “an app instance per test case”.
In practice, MSW inside a shared Next.js server gives every parallel test the same responses. Two tools add per-test handlers on top of MSW. Next.js test mode accepts MSW handlers (with MSW 2, see above). scenarist keys scenarios by an x-scenarist-test-id header that your server code forwards on each fetch (Next.js adapter).
A forward proxy such as mockttp
Section titled “A forward proxy such as mockttp”When the Playwright team closed #30766, they pointed to Simon Knott’s post. It puts mockttp, an HTTP and HTTPS mock server and proxy, in front of the server’s outgoing traffic. Each test adds rules to it, such as mocks.forPost(url).thenJson(200, body).
The app’s code doesn’t change. Its server process starts with environment variables that send outgoing requests through the proxy:
HTTP_PROXYandHTTPS_PROXYset to the proxy’s URL.NODE_USE_ENV_PROXY=1, so Node’s built-infetchuses those variables. Node.js added it in 24.0.0 and 22.21.0, and marks it as active development (Node.js docs).NODE_EXTRA_CA_CERTSpointing at the proxy’s CA certificate, so the server trusts the certificates mockttp generates for HTTPS hosts.
The proxy belongs to one Playwright worker, so the post starts one app server per worker from a worker fixture, each on a free port. It avoids Playwright’s webServer, which starts one process for the whole run before any worker exists. In the post’s setup, requests with no rule pass through to the real API. The post suggests answering them with an error instead, so a forgotten mock shows up.
The Playwright team describes the approach as “agnostic of the server language and framework”. The post itself uses a plain Node server, not Next.js or TanStack Start.
Record real responses with test-proxy-recorder
Section titled “Record real responses with test-proxy-recorder”With the three options above, you write every response yourself. test-proxy-recorder records what your real API returns during a local run, then replays it on CI.
- The proxy. Start
test-proxy-recorder <target-url>next to your app for the test run. Point the app’s API base URL at it whileTEST_PROXY_RECORDER_ENABLEDis set. Each proxy forwards to one backend, the<target-url>it was started with. registerProxyFetch(). One call on the server patches the globalfetch. It copies the current request’sx-test-rcrd-idheader onto every outgoing request. The proxy reads that header to file each server-side call under its test. The call is a no-op in production unlessTEST_PROXY_RECORDER_ENABLEDis set.- The per-test id.
playwrightProxy.before(page, testInfo, mode)builds a session id from the spec file, itsdescribetitles and the test title. It sends that id asx-test-rcrd-idon the page’s requests and switches that session torecordorreplay. Parallel workers share one app server and one proxy, and each test still gets its own recording.
import { registerProxyFetch } from 'test-proxy-recorder/nextjs';
registerProxyFetch();import { registerProxyFetch } from 'test-proxy-recorder/tanstack-start';
registerProxyFetch();test.beforeEach(async ({ page }, testInfo) => { await playwrightProxy.before(page, testInfo, 'replay', { url: /localhost:8100/ });});Record once against the real API and commit the .mock.json (server) and .har (browser) files. CI then replays them with the backend off. In replay, a request with no recording gets a 404 that names it. It never reaches the API.
Recorded responses only cover what the API did while you recorded. To force an error or an edge case the API won’t produce on demand, a hand-written handler from one of the options above is simpler.
Setup details are in the Next.js and TanStack Start guides.
Compare the options
Section titled “Compare the options”| Option | Setup in the app | Parallel tests | Handlers or recorded responses | Next.js and TanStack Start |
|---|---|---|---|---|
| Next.js test mode | experimental.testProxy in next.config.js | Yes, handlers are kept per test | Handlers (next.onFetch(), or MSW 2 handlers) | Next.js only |
| MSW in the server | setupServer started in server code (the root layout in MSW’s example) | One handler set for every test on that server | Handlers | Next.js: an example in an open pull request. TanStack Start: not documented |
| mockttp | No code change. Proxy variables and a CA certificate on the server process | Yes, with one app server per worker | Handlers. Requests with no rule pass through | Not documented. The Playwright team calls it framework-agnostic |
| test-proxy-recorder | API base URL pointed at the proxy during tests, plus registerProxyFetch() | Yes, with one shared app server | Recorded from the real API | Both documented |
The comparison covers more tools, including Mocky Balboa, scenarist, Polly.js, talkback and proxay, and the browser-side options.