VCR for Playwright

Record once.
Replay forever.

Captures real API responses while your Playwright suite runs locally, then replays them byte-for-byte on CI. No backend, no network, no hand-written mocks to maintain.

npm i -D test-proxy-recorder Star on GitHubGitHub star count

MIT · TypeScript · works with Next.js & TanStack Start SSR, SPAs & Chrome extensions · WebSocket support

REC · MODE=record · local run

saved e2e/recordings/todos.GET.mock.json

REPLAY · MODE=replay · CI

200 OK · 3 ms · zero network

See it record, then replay

One Playwright run records real responses to disk; flip to replay and the same suite passes with the backend off — no network.

Two recorders, one proxy

Requests originate in two places, so there are two recording mechanisms. Use either — or both together. Both record once and replay from disk, so CI runs with the backend off and no hand-written mocks.

Proxy

.mock.json

Next.js / TanStack Start SSR → proxy → real API

Sits between your server and the API. Records server-side requests — SSR fetches, route handlers, anything your backend-for-frontend calls.

For full-stack apps where the server calls the API.

View Next.js example →View TanStack Start example →

HAR

.har

browser → HAR intercept → real API

Intercepts in the browser itself. Records client-side fetchcalls, Chrome extension API traffic, analytics, third-party APIs.

For SPAs, extensions, and browser-only apps.

View Chrome extension example →

Where it fits

The mocking tools are good at different jobs. The combination below — recording real traffic across SSR, browser, and WebSockets, with no hand-written mocks — is the gap the others leave open.

Feature comparison of test-proxy-recorder against Playwright routeFromHAR, MSW, Polly.js, playwright-network-cache, and Mocky Balboa.
Featuretest-proxy-recorderPlaywright routeFromHARMSWPolly.jsplaywright-network-cacheMocky Balboa
Record real trafficYesYesNoYesYesNo
Server-side (SSR)YesNoYesPartialNoYes
Browser-sideYesYesYesYesYesYes
WebSocketYesNoYesNoNoNo
Playwright-nativeYesYesNoNoYesYes
MaintainedYesYesYesNoYesYes

Polly.js intercepts Node HTTP, so SSR mocking is possible inside the app process, but not as part of a Playwright run. MSW and Mocky Balboa replay real responses too — but you hand-write the mocks. When to reach for something else is covered in thedocs.

Works with your real auth provider

Log in through Cognito, Auth0, Clerk, or WorkOS — for real, on every run. Only your app's API is recorded; auth stays live, your data goes offline.

// e2e/auth.setup.ts — log in for real, once. Never recorded.
import { test as setup } from '@playwright/test';
import { setProxyMode } from 'test-proxy-recorder';
setup('authenticate', async ({ page }) => {
await setProxyMode('transparent'); // login bypasses the recorder
await page.goto('/login');
await page.getByTestId('email').fill(process.env.TEST_EMAIL!);
await page.getByTestId('password').fill(process.env.TEST_PASSWORD!);
await page.getByTestId('signinButton').click();
await page.waitForURL('/dashboard');
// Reused by every test — they start already signed in.
await page.context().storageState({ path: 'e2e/.auth/state.json' });
});

Set up in three steps

Scaffold everything with one command, point your API at the proxy, then record and commit. Browser-only app? initskips the SSR step for you.

Fastest path: hand it to your AI agent

Copy this, swap in your backend URL, and paste it into Claude Code, Cursor, or any coding agent — it runs initand finishes the wiring from the prompt initprints.

Set up test-proxy-recorder for end-to-end tests in this project, then follow the
instructions that `init` prints. Run these commands:
npx @tanstack/intent@latest install
npm install --save-dev test-proxy-recorder
Then run init, passing this project's backend API base URL as the target — find
it yourself from the app's env/config (the URL the app calls in dev); don't
assume the default:
npx test-proxy-recorder init <your-backend-api-url> --port 8100 --dir ./e2e/recordings
Then complete the app-specific steps init prints: point the app's API base URL at
the proxy in dev/test only, tag server-side fetches (Next.js), add a smoke test,
and verify record → replay.

Or wire it up by hand:

  1. Install & scaffold

    init writes the proxy config, a Playwright fixture, a global teardown,package.json scripts, and (on Next.js) wires SSR fetch tagging into your root layout — non-destructively.

    Terminal window
    npm install --save-dev test-proxy-recorder
    # http://localhost:3002 is your API endpoint; 8100 is the proxy. Flags are optional.
    npx test-proxy-recorder init http://localhost:3002 --port 8100 --dir ./e2e/recordings
  2. Point your app's API at the proxy

    The one thing initcan't guess: which env var holds your API base URL. Point it at the proxy when the recorder is enabled, at the real backend otherwise — the proxy never runs in production.

    // Point your app at the proxy when the recorder is enabled, at the real backend otherwise.
    // The proxy never runs in production — TEST_PROXY_RECORDER_ENABLED is set only for e2e.
    const API_BASE =
    process.env.NODE_ENV === 'production' && !process.env.TEST_PROXY_RECORDER_ENABLED
    ? 'https://api.example.com'
    : 'http://localhost:8100'; // proxy address from `init`
    const res = await fetch(`${API_BASE}/todos`);

    On Next.js, initalso adds registerProxyFetch()to your root layout to tag server-side fetchcalls — a no-op in production:

    // app/layout.tsx — tag server-side fetches so SSR is recorded/replayed
    import { registerProxyFetch } from 'test-proxy-recorder/nextjs';
    registerProxyFetch(); // no-op in production unless TEST_PROXY_RECORDER_ENABLED=true
  3. Record, commit, replay

    Set MODE = 'record', run once against the real API, then flip to 'replay'and commit. Recordings live in git — that's what makes CI deterministic. Don't gitignore them.

    e2e/my.test.ts
    import { test, expect } from '@playwright/test';
    import { playwrightProxy } from 'test-proxy-recorder';
    // Full-stack: the browser also talks to the proxy, so match its URL.
    // (Browser-only app? Match your real API domain instead, e.g. /api\.example\.com/.)
    const CLIENT_SIDE_URL = /localhost:8100/;
    // 'record' hits the real API and saves responses.
    // 'replay' serves them from disk — no network needed.
    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
    # 1. Set MODE = 'record' in your test file, run against the real API
    npx playwright test --ui # recordings written to e2e/recordings/
    # 2. Flip MODE back to 'replay' and commit the recordings
    git add e2e/recordings/
    git commit -m "add e2e recordings"

Stop hand-writing mocks

Your API already gives the right answers. Record them.

npm i -D test-proxy-recorder Star on GitHub

If it saved you an afternoon, a star takes one second — it's how the next person finds it, and it tells a solo maintainer to keep building. Hit a snag or have an idea? Open an issueorjoin Discord.