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 GitHub GitHub star count

MIT · TypeScript · works with Next.js 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 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 →

HAR

.har

browser → HAR intercept → real API

Intercepts in the browser itself. Records client-side fetch calls, 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.
Feature test-proxy-recorder Playwright routeFromHAR MSW Polly.js playwright-network-cache Mocky Balboa
Record real traffic Yes Yes No Yes Yes No
Server-side (SSR) Yes No Yes Partial No Yes
Browser-side Yes Yes Yes Yes Yes Yes
WebSocket Yes No Yes No No No
Playwright-native Yes Yes No No Yes Yes
Maintained Yes Yes Yes No Yes Yes

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 the docs.

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? init skips 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 init and finishes the wiring from the prompt init prints.

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 init can'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, init also adds registerProxyFetch() to your root layout to tag server-side fetch calls — 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 issue or join Discord.