Skip to content

Manual setup

Most people should run init — it writes every file below for you. This page is the reference for what init generates, so you can wire it up by hand, drop codegen, or understand each piece.

For Next.js and similar frameworks, where both the server and the browser make API calls. Use both recording mechanisms together — see how it works.

The proxy is a lightweight process you start alongside your app for the test run (via a script, as below, or Playwright’s webServer) — it’s not infrastructure you deploy or maintain. The whole setup is: start it next to your app, point your app’s API base URL at it, propagate the session header from SSR, and write one fixture.

{
"scripts": {
"proxy": "test-proxy-recorder http://localhost:8000 --port 8100 --dir ./e2e/recordings",
"dev:proxy": "concurrently \"npm run proxy\" \"TEST_PROXY_RECORDER_ENABLED=1 npm run dev\"",
"serve:proxy": "concurrently \"npm run proxy\" \"TEST_PROXY_RECORDER_ENABLED=1 npm run serve\""
}
}

In your app code, point the API base URL at the proxy when the recorder is enabled, at the real backend otherwise — the proxy never runs in production:

const API_BASE =
process.env.NODE_ENV === 'production' && !process.env.TEST_PROXY_RECORDER_ENABLED
? 'https://api.example.com'
: 'http://localhost:8100'; // proxy address

TEST_PROXY_RECORDER_ENABLED is set by the dev:proxy / serve:proxy scripts above, and by init’s generated scripts. Use whatever env var your app already uses for the API base URL (for example API_URL, NEXT_PUBLIC_API_URL) — the same conditional applies.

Server-side fetch calls need the recording-session header so the proxy knows which test they belong to. Playwright already sets it on the browser navigation, so the id is in next/headers — you just attach it to outgoing SSR requests. Add one line to your root layout (init does this for you):

app/layout.tsx
import { registerProxyFetch } from 'test-proxy-recorder/nextjs';
registerProxyFetch(); // no-op in production unless TEST_PROXY_RECORDER_ENABLED=true
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body>{children}</body>
</html>
);
}

This works on the Node and Edge runtimes. For axios apps, call registerProxyAxios(instance) on each server-side instance instead; for a single fetch, createHeadersWithRecordingId(await headers()) is a patch-free alternative. A proxy.ts/middleware.ts with setNextProxyHeaders is optional — it only exposes the id, it doesn’t tag fetches. Record against a production build (next build && next start), not next dev. See the Next.js integration for details. Browser-only apps can skip this step.

import { test, expect } from '@playwright/test';
import { playwrightProxy } from 'test-proxy-recorder';
// SSR requests (server → proxy) are recorded to .mock.json.
// Browser requests to the proxy URL are also covered.
const CLIENT_SIDE_URL = /localhost:8100/;
// Change to 'record' to update recordings.
const MODE = 'replay' as const;
test.beforeEach(async ({ page }, testInfo) => {
await playwrightProxy.before(page, testInfo, MODE, { url: CLIENT_SIDE_URL });
});
test('homepage loads', async ({ page }) => {
await page.goto('/');
await expect(page.getByText('Welcome')).toBeVisible();
});
Terminal window
# Terminal 1
npm run serve:proxy
# Terminal 2 — .mock.json and .har files are written automatically
npx playwright test
Terminal window
git add e2e/recordings/
git commit -m "add e2e recordings"

When all API calls come from the browser (no SSR), you only need the HAR mechanism. No proxy backend is required for the actual recording — the proxy process just provides session management.

Terminal window
npm install --save-dev test-proxy-recorder
import { defineConfig } from '@playwright/test';
export default defineConfig({
webServer: {
command: 'test-proxy-recorder https://api.example.com --port 8100 --dir ./e2e/recordings',
url: 'http://localhost:8100/__control',
reuseExistingServer: true,
},
});

The proxy target (https://api.example.com) does not matter for browser-only recording — it is only used if server-side (SSR) requests also need to be proxied. The proxy process must run so its /__control endpoint is available for session management.

e2e/fixtures.ts
import { test as base, type Page, type BrowserContext } from '@playwright/test';
import { playwrightProxy } from 'test-proxy-recorder';
// Match the external API domain your browser makes requests to.
// In record mode these requests go to the real API and are saved.
// In replay mode they are served from disk — no network needed.
const CLIENT_SIDE_URL = /api\.example\.com/;
// Change to 'record' to hit the real API and update recordings.
const MODE = 'replay' as const;
export const test = base.extend<{ page: Page }>({
page: async ({ context }, use, testInfo) => {
const page = await context.newPage();
await playwrightProxy.before(page, testInfo, MODE, { url: CLIENT_SIDE_URL });
await use(page);
},
});
e2e/my.test.ts
import { test, expect } from './fixtures';
test('homepage loads', async ({ page }) => {
await page.goto('https://myapp.com/');
await expect(page.getByText('Welcome')).toBeVisible();
});

5. Record — run once against the real API

Section titled “5. Record — run once against the real API”
Terminal window
# In fixtures.ts: const MODE = 'record' as const;
npx playwright test
# .har files are written to e2e/recordings/ automatically
Terminal window
# In fixtures.ts: const MODE = 'replay' as const;
git add e2e/recordings/
git commit -m "add e2e recordings"

CI now runs without any network access.

Add this to .gitattributes to collapse large recording files in PR diffs:

/e2e/recordings/** binary