Network Interception and API Mocking in Playwright (page.route)

With network interception, Playwright can fake API responses, simulate server errors, block ads and record traffic. In this lesson you will learn page.route step by step with examples on Playwright’s official API-mocking demo page.

Advertisement

📖 What You’ll Learn: what network interception is, page.route patterns, mocking responses with fulfill, modifying real responses with route.fetch, simulating errors and slow APIs, changing requests, blocking resources, request and response events, context.route, HAR record and replay, and when to mock.


🛰️ What Is Network Interception?

Every modern web page talks to a backend through network requests: fetching products, submitting forms, loading images. Playwright can sit between the browser and the network and:

  • Observe requests and responses (what was called, with which data).
  • Mock responses: return your own data instead of calling the real server.
  • Modify real responses before the page sees them.
  • Block requests you do not need, such as ads, analytics or large images.

This lets you test situations that are hard to create for real: empty lists, server errors, slow APIs, or a feature whose backend is not finished yet.


🧭 page.route(): The Core API

await page.route(urlPattern, async route => {
  // decide what to do with each matching request:
  // route.fulfill(...)  → respond with mock data
  // route.continue(...) → send it to the real server (optionally modified)
  // route.abort()       → block it
});

The URL pattern can be a glob ('**/api/products'), a regular expression (/\/api\/v\d\/fruits/) or a function. Register routes before the page makes the request, usually before page.goto().


🍓 Mock an API Response

Playwright’s official demo page https://demo.playwright.dev/api-mocking loads a list of fruits from /api/v1/fruits. Replace the real list with your own:

import { test, expect } from '@playwright/test';

test('shows mocked fruit', async ({ page }) => {
  await page.route('*/**/api/v1/fruits', async route => {
    const json = [{ name: 'Strawberry', id: 21 }];
    await route.fulfill({ json });              // status 200 with a JSON body
  });

  await page.goto('https://demo.playwright.dev/api-mocking');
  await expect(page.getByText('Strawberry')).toBeVisible();
});

route.fulfill() options:

OptionPurpose
jsonBody as an object, sets the JSON content type automatically
statusHTTP status code, e.g. 500
bodyRaw string or Buffer body
contentTypee.g. 'text/html'
headersResponse headers
pathServe the body from a local file

✏️ Modify a Real Response

Fetch the real response, change it, then return it. This keeps the test close to reality while controlling one detail:

test('adds a fruit to the real list', async ({ page }) => {
  await page.route('*/**/api/v1/fruits', async route => {
    const response = await route.fetch();       // real request
    const json = await response.json();
    json.push({ name: 'Loquat', id: 100 });     // add one item
    await route.fulfill({ response, json });    // keep real status and headers
  });

  await page.goto('https://demo.playwright.dev/api-mocking');
  await expect(page.getByText('Loquat', { exact: true })).toBeVisible();
});

💥 Simulate Errors, Empty Data and Slow Responses

Use mocks to test how your application handles problems. Point these at your own app’s endpoints and assert its error or empty states:

// Server error
await page.route('**/api/orders', route => route.fulfill({ status: 500, body: 'Internal Server Error' }));

// Empty list
await page.route('**/api/orders', route => route.fulfill({ json: [] }));

// Network failure
await page.route('**/api/orders', route => route.abort('failed'));

// Slow API: wait 3 seconds before continuing to the real server
await page.route('**/api/orders', async route => {
  await new Promise(resolve => setTimeout(resolve, 3000));
  await route.continue();
});

💡 Tip: Error-state tests are where mocking shines. Ask: what should the user see when the API returns 500, an empty array, or takes 10 seconds? Then write a test for each.


🔧 Modify Outgoing Requests

route.continue() can change the request before it reaches the server:

await page.route('**/api/**', async route => {
  const headers = { ...route.request().headers(), 'x-test-run': 'playwright' };
  await route.continue({ headers });
});

You can also change method, postData or url, for example to point one endpoint at a staging server.


🚫 Block Unneeded Resources

Blocking images, fonts or third-party scripts can make tests faster and more stable:

test.beforeEach(async ({ page }) => {
  await page.route('**/*.{png,jpg,jpeg,gif,svg,webp}', route => route.abort());
  await page.route(/google-analytics|googletagmanager|doubleclick/, route => route.abort());
});

⚠️ Common mistake: Do not block resources in tests that check layout or images. Blocking is for functional tests where those resources do not matter.


👂 Listen to Requests and Responses

To observe traffic without changing it, use events:

page.on('request', request => console.log('>>', request.method(), request.url()));
page.on('response', response => console.log('<<', response.status(), response.url()));
page.on('requestfailed', request => console.log('FAILED', request.url(), request.failure()?.errorText));

To wait for one specific call and assert its data, use page.waitForResponse(), explained in Playwright waits:

const responsePromise = page.waitForResponse('**/api/v1/fruits');
await page.goto('https://demo.playwright.dev/api-mocking');
const response = await responsePromise;
expect(response.status()).toBe(200);

🌍 Routes for All Pages: context.route()

page.route() applies to one tab. context.route() applies to every page in the browser context, including popups and new tabs:

test('mock for every tab', async ({ context, page }) => {
  await context.route('**/api/config', route => route.fulfill({ json: { featureX: true } }));
  await page.goto('https://example.com');
});

Remove a route with page.unroute(pattern) or page.unrouteAll().


📼 Record and Replay with HAR Files

A HAR file is a recording of network traffic. Record real responses once, then replay them in tests so they do not depend on the backend:

// First run: record (update: true) the API traffic into a HAR file
await page.routeFromHAR('./hars/fruits.har', { url: '*/**/api/v1/fruits', update: true });
await page.goto('https://demo.playwright.dev/api-mocking');

// Later runs: remove update: true to replay the saved responses
// await page.routeFromHAR('./hars/fruits.har', { url: '*/**/api/v1/fruits' });

⚖️ When to Mock (and When Not To)

Mock itUse the real backend
Error, empty and edge-case statesCritical end-to-end business flows
Third-party services (payments, maps, SMS)Smoke tests before a release
Features whose backend is not readyContract between frontend and backend
Slow or rate-limited APIsData that must really be saved

A healthy suite has both: a few full end-to-end tests against real services, and many focused tests that mock the network.


📝 Assignment

Use https://demo.playwright.dev/api-mocking:

  1. Mock /api/v1/fruits to return three fruits of your choice and assert all three are visible.
  2. Modify the real response by adding a fruit with route.fetch() and assert it appears.
  3. Mock an empty array and describe what the page shows.
  4. Log every request and response URL while the page loads.
  5. Use page.waitForResponse to assert the fruits API returns status 200.
  6. On https://www.saucedemo.com block all images with route.abort() and log in; confirm the test still passes.
  7. Bonus: record the fruits API into a HAR file and replay it in a second test.

❓ FAQ

🤔 How do I mock an API response in Playwright?

Register a route before navigation and fulfill it: await page.route('**/api/items', route => route.fulfill({ json: [...] })).

🤔 What is the difference between route.fulfill, route.continue and route.abort?

fulfill answers the request with your own response, continue sends it to the real server (optionally modified), and abort blocks it.

🤔 How do I modify a real API response in Playwright?

Call const response = await route.fetch(), change the JSON, then route.fulfill({ response, json }).

🤔 How do I block images or ads in Playwright?

Use a route with route.abort(), for example page.route('**/*.{png,jpg}', r => r.abort()).

🤔 Does page.route work for API tests with the request fixture?

No. Routes intercept traffic from the browser page. Requests sent with the request fixture go directly to the server; see API testing with Playwright.


🎯 Next Steps

🎬 Watch the full Playwright tutorial series on YouTube!

Subscribe to SqaToolsByDY • Practice on sqatools.in

2 thoughts on “Network Interception and API Mocking in Playwright (page.route)”

Leave a Comment