Writing mock data by hand is tedious. What if you could record all the API responses from a real session and replay them in your tests? That is exactly what HAR files do. HAR stands for HTTP Archive. It is a JSON file that captures every request and response. Playwright can record them and play them back.
Open DevTools, go to Network tab, right-click, "Save all as HAR." That file contains every request URL, method, headers, body, status code, and response body. Playwright uses the same format.
import { test } from '@playwright/test';
test('record HAR for shopping flow', async ({ page }) => {
// Start recording all network traffic to a HAR file
await page.routeFromHAR('tests/fixtures/shopping.har', {
update: true, // This means RECORD mode
});
// Do the real flow -- all API calls are recorded
await page.goto('/shopping');
await page.getByRole('link', { name: 'Electronics' }).click();
await page.getByRole('button', { name: 'Add to Cart' }).first().click();
// HAR file is saved when the test finishes
// Run this test ONCE with a real backend, then use the HAR for all future runs
});test('use recorded HAR for shopping test', async ({ page }) => {
// Play back the recorded responses -- no real backend needed
await page.routeFromHAR('tests/fixtures/shopping.har', {
update: false, // PLAYBACK mode (default)
notFound: 'fallback', // Unrecorded URLs go to real server
});
await page.goto('/shopping');
await page.getByRole('link', { name: 'Electronics' }).click();
// All API responses come from the HAR file
// Test runs without any backend
});Run test with update: true against real backend. HAR file is created.
Commit the HAR file to your repo (tests/fixtures/ folder).
Change update to false. Now the test runs from the HAR file.
If the API changes, re-record by setting update: true again.
Use notFound: "fallback" to let unrecorded URLs hit the real server.
| Option | Value | Description |
|---|---|---|
| update | true | Record mode -- saves real responses to HAR file |
| update | false | Playback mode -- uses saved responses (default) |
| notFound | "abort" | Abort if URL not found in HAR (strict) |
| notFound | "fallback" | Let unrecorded URLs go to real server (lenient) |
| url | glob or regex | Only record/replay URLs matching this pattern |
test('only mock API calls from HAR, let static assets load normally', async ({ page }) => {
await page.routeFromHAR('tests/fixtures/api-only.har', {
url: '**/api/**', // Only mock API routes
notFound: 'fallback', // Everything else goes to real server
});
await page.goto('/shopping');
// Static assets (CSS, JS, images) load from real server
// API calls use recorded responses
});HAR files can be large. They capture response bodies including images and scripts. Use the url option to limit what gets recorded. Also add *.har to .gitignore if the files are too large, and store them in CI artifact storage instead.
Q: When would you choose HAR recording over manual mocking?
A: HAR recording is great when I have many API calls in a flow and I need realistic data shapes. For example, a checkout flow that calls 8 different APIs. Writing manual mocks for all 8 is tedious and error-prone. I record once and replay. But for specific edge case testing (empty cart, expired session, server error), manual mocking gives me precise control. In practice, I use both -- HAR for the happy path baseline, manual mocks for edge cases.
Key Point: Record real API responses into HAR files with update: true. Replay them in tests with update: false. No more writing mock data by hand for complex flows.