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.
Full-stack (SSR + browser)
Section titled “Full-stack (SSR + browser)”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.
1. Add scripts to package.json
Section titled “1. Add scripts to package.json”{ "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 addressTEST_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.
2. Tag server-side fetches (Next.js)
Section titled “2. Tag server-side fetches (Next.js)”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):
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.
3. Write a test
Section titled “3. Write a test”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();});4. Record
Section titled “4. Record”# Terminal 1npm run serve:proxy
# Terminal 2 — .mock.json and .har files are written automaticallynpx playwright test5. Switch to replay and commit
Section titled “5. Switch to replay and commit”git add e2e/recordings/git commit -m "add e2e recordings"Browser-only / SPA / extension
Section titled “Browser-only / SPA / extension”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.
1. Install
Section titled “1. Install”npm install --save-dev test-proxy-recorder2. Add the proxy to playwright.config.ts
Section titled “2. Add the proxy to playwright.config.ts”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.
3. Write a fixture
Section titled “3. Write a fixture”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); },});4. Write a test
Section titled “4. Write a test”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”# In fixtures.ts: const MODE = 'record' as const;npx playwright test# .har files are written to e2e/recordings/ automatically6. Switch to replay and commit
Section titled “6. Switch to replay and commit”# 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