A Guide to Mocking API Requests in Playwright: From Basics to Advanced

August 5, 2025

Imagine your critical end-to-end test suite runs, and a key test fails. The culprit isn't a bug in your application, but a staging API that's down for maintenance or returning an unexpected 503 error. This scenario is frustratingly common and highlights a fundamental challenge in test automation: dependency on external services. When your frontend tests rely on live backends, they inherit all the backend's instability, latency, and data inconsistencies. The solution lies in taking control of the network layer directly within your tests. This is where mastering the playwright mock api becomes not just a useful skill, but a transformative practice for building a robust, fast, and deterministic testing strategy. By intercepting and faking API responses, you can isolate your application under test, simulate any possible server state on demand, and run your test suite with unparalleled speed and reliability. This guide will take you on a deep dive into the world of API mocking with Playwright, from the fundamental concepts to advanced patterns that will elevate your testing capabilities.

The 'Why': The Critical Role of API Mocking in Modern E2E Testing

In a perfect world, every end-to-end test would run against a production-like environment with 100% uptime and predictable performance. In reality, testing environments are often shared, unstable, and slow. Relying on live API calls during automated testing introduces significant variables that can undermine the very purpose of the tests: to find bugs in your application, not in the infrastructure. The practice of using a playwright mock api directly addresses these challenges by decoupling the frontend from the backend during the test execution lifecycle.

Let's break down the core problems that API mocking solves:

  • Test Flakiness and Reliability: This is the most significant pain point. A test that passes sometimes and fails other times without any code changes is considered 'flaky'. A study from Google researchers on flaky tests highlights how non-determinism, often caused by asynchronous network calls, is a primary contributor. When your Playwright test depends on a real network request, it's subject to server downtime, network latency, rate limiting, or transient errors. Mocking eliminates this entire class of failures, ensuring that a test only fails if there's a genuine issue in the front-end code.

  • Execution Speed: Modern development cycles demand rapid feedback. The State of DevOps Report consistently links elite performance with fast, reliable automated testing. Waiting for real network responses, which can take hundreds of milliseconds or even seconds per call, adds up dramatically across a large test suite. A test that takes 30 seconds with live APIs might take only 3 seconds with mocked responses. This 10x improvement means faster CI/CD pipelines, quicker feedback for developers, and ultimately, a more agile development process.

  • Testing Edge Cases and Error States: How do you test your application's beautiful '500 Internal Server Error' page if you can't easily force the server to produce that error on demand? How do you test the UI's behavior when an API returns an empty array, a malformed JSON object, or a specific error code like 401 Unauthorized? With a playwright mock api, you can simulate these scenarios with surgical precision. You can craft specific responses to test every possible state, including loading spinners, error messages, and retry logic—conditions that are often difficult or impossible to reproduce consistently against a live backend.

  • Cost and Rate Limiting: Many applications rely on third-party APIs that come with usage costs or strict rate limits. Running a full E2E test suite multiple times a day could incur significant costs or get your test environment temporarily blocked. By mocking these third-party calls, you can test your application's integration logic without ever hitting the actual service, saving money and avoiding lockout issues.

  • Development in Parallel: Front-end and back-end teams often work in parallel. The front-end team might need to build UI components that depend on API endpoints that haven't been built yet. By agreeing on an API contract (a predefined structure for the request and response), the front-end team can use Playwright's mocking features to build and test their components against a simulated API, completely unblocking their development workflow.

The Core Engine: A Deep Dive into Playwright's `page.route()`

The heart of Playwright's network interception capability is the page.route() function. This powerful method allows you to intercept network requests that match a specific URL pattern and decide what to do with them. It's the central command from which all playwright mock api strategies originate. Understanding its signature and the objects it works with is fundamental to effective mocking.

The method signature is page.route(url, handler). Let's dissect its components:

  • url: This parameter defines which network requests you want to intercept. It's incredibly flexible and can be a string, a regular expression, or a glob pattern. This allows for both broad and highly specific targeting.

    • String: page.route('https://api.myapp.com/users', ...) will match that exact URL.
    • Glob Pattern: page.route('**/api/v1/users**', ...) will match any URL containing that path segment, regardless of the domain or query parameters. This is often the most practical choice.
    • Regular Expression: page.route(/\/api\/v[12]\/products/, ...) provides the ultimate flexibility for matching complex URL patterns.
  • handler(route, request): This is an asynchronous callback function that executes whenever a matching request is intercepted. It receives two crucial arguments:

    • request: An object containing all the information about the original outgoing request, such as its URL, headers, method (GET, POST, etc.), and body (request.postData()). You use this to inspect the request.
    • route: An object that gives you the power to control the intercepted request. You can use it to decide the fate of the request: fulfill it with mock data, let it continue to the network, or abort it entirely.

Let's explore the key methods of the route object, as detailed in the official Playwright documentation.

route.fulfill(options): The Mocking Workhorse

This is the most common method you'll use. It fulfills the request with a custom, mocked response, preventing it from ever hitting the network. It takes an options object with several useful properties:

  • status: The HTTP status code to return (e.g., 200, 404, 500).
  • headers: An object of HTTP response headers.
  • contentType: A shortcut for setting the Content-Type header (e.g., 'application/json').
  • body: The response body as a string or Buffer.
  • json: A convenient way to provide a JavaScript object that will be automatically stringified and sent with the Content-Type header set to application/json.
  • path: Fulfills the request with the contents of a local file.

Example: Fulfilling with JSON

// test.spec.js
await page.route('**/api/users', route => {
  console.log('Intercepted:', route.request().url());
  route.fulfill({
    status: 200,
    contentType: 'application/json',
    json: [
      { id: 1, name: 'John Doe' },
      { id: 2, name: 'Jane Smith' }
    ]
  });
});

route.abort([errorCode]): Simulating Failures

Sometimes you want to simulate a complete network failure or block unwanted requests (like third-party analytics scripts). route.abort() does exactly that.

You can optionally provide an errorCode to simulate specific network-level failures, such as 'connectionrefused' or 'timedout'. A list of valid error codes is available in Playwright's API documentation.

Example: Blocking Analytics

// Block all requests to Google Analytics
await page.route('**/*google-analytics.com/**', route => {
  console.log('Aborting request to:', route.request().url());
  route.abort();
});

route.continue([overrides]): Modifying and Proceeding

What if you don't want to completely fake a response, but rather modify a real one? route.continue() allows the request to proceed to the network. More powerfully, it accepts an overrides object where you can change aspects of the outgoing request, such as its URL, method, headers, or post data, before it's sent.

This is an advanced use case, often combined with page.request.fetch() to intercept a response, modify its body, and then fulfill the route. We'll explore this pattern in the advanced techniques section. A proper understanding of HTTP as described by resources like MDN Web Docs is essential for using this effectively.

From Theory to Practice: Common Playwright Mock API Scenarios

With a solid understanding of page.route(), we can now apply it to solve real-world testing problems. The true value of any playwright mock api strategy is demonstrated through its practical application. Here are five common scenarios you'll encounter and how to handle them effectively.

Scenario 1: Mocking a Successful GET Request

This is the bread and butter of API mocking. Your application loads a page and immediately fetches data to display. Your test needs to provide this data reliably.

Use Case: Testing a user list page that fetches data from /api/users.

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

test('should display a list of users', async ({ page }) => {
  // Arrange: Set up the mock before navigating
  await page.route('**/api/users', async route => {
    const mockUserData = [
      { id: 'u1', name: 'Alice' },
      { id: 'u2', name: 'Bob' }
    ];
    await route.fulfill({ json: mockUserData });
  });

  // Act: Navigate to the page that triggers the API call
  await page.goto('/users');

  // Assert: Check if the UI renders the mocked data
  await expect(page.getByText('Alice')).toBeVisible();
  await expect(page.getByText('Bob')).toBeVisible();
});

Scenario 2: Simulating API Errors

Robust applications handle server errors gracefully. You need to test that your application shows a proper error message instead of crashing or showing a blank screen when an API fails.

Use Case: Verify that a user-friendly error message is shown when the server returns a 500 error.

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

test('should display an error message on API failure', async ({ page }) => {
  // Arrange: Mock a 500 server error
  await page.route('**/api/dashboard-data', async route => {
    await route.fulfill({
      status: 500,
      contentType: 'application/json',
      json: { message: 'Internal Server Error' }
    });
  });

  // Act
  await page.goto('/dashboard');

  // Assert
  await expect(page.getByText('Oops! Something went wrong.')).toBeVisible();
  await expect(page.getByText('Loading data...')).not.toBeVisible();
});

Scenario 3: Handling and Verifying POST Requests

When a user submits a form, you want to verify that the application sends the correct data to the backend, without actually needing the backend to process it. You can inspect the request's payload before returning a mock success response.

Use Case: Testing a new user creation form.

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

test('should send correct data when creating a new user', async ({ page }) => {
  const expectedPayload = { name: 'Charlie', email: '[email protected]' };

  // Arrange: Intercept the POST request
  await page.route('**/api/users', async route => {
    // Assert: Check if the request method and payload are correct
    expect(route.request().method()).toBe('POST');
    expect(route.request().postDataJSON()).toEqual(expectedPayload);

    // Fulfill with a success response
    await route.fulfill({ json: { id: 'u3', ...expectedPayload } });
  });

  // Act: Fill and submit the form
  await page.goto('/users/new');
  await page.getByLabel('Name').fill(expectedPayload.name);
  await page.getByLabel('Email').fill(expectedPayload.email);
  await page.getByRole('button', { name: 'Create' }).click();

  // Assert: Check for success message in the UI
  await expect(page.getByText('User Charlie created successfully!')).toBeVisible();
});

Scenario 4: Modifying a Live API Response

This is a more advanced but powerful technique. Sometimes, you want to test against a real API but need to tweak the response data for a specific edge case. You can achieve this by letting the request go through, capturing the response, modifying it, and then fulfilling the route with your modified data. This requires using Playwright's built-in request context.

Use Case: Test how the UI handles a user object that is missing an optional avatarUrl property.

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

test('should display a default avatar if avatarUrl is missing', async ({ page }) => {
  await page.route('**/api/users/current', async route => {
    // Fetch the actual response from the server
    const response = await page.request.fetch(route.request());
    const json = await response.json();

    // Modify the response body: remove the avatarUrl property
    delete json.avatarUrl;

    // Fulfill the route with the modified response
    await route.fulfill({ response, json });
  });

  await page.goto('/profile');

  // Assert that the default avatar is now visible
  await expect(page.locator('img[src="/default-avatar.png"]')).toBeVisible();
});

This pattern, as noted in many software design articles, helps keep mocks closely aligned with reality while still allowing for targeted testing of UI logic.

Leveling Up: Advanced Playwright Mock API Techniques & Best Practices

Once you've mastered the basics, you can adopt more sophisticated patterns to make your playwright mock api implementation cleaner, more powerful, and easier to maintain. These techniques separate good test suites from great ones.

Best Practice: Organizing and Reusing Mocks

A common pitfall is cluttering individual test files with repetitive page.route setup code. This violates the DRY (Don't Repeat Yourself) principle and makes maintenance a headache. A better approach is to centralize your mock configurations.

Strategy: Create a dedicated mocks directory within your test folder. Inside, define helper functions that encapsulate the setup for specific features or endpoints.

// tests/mocks/user-mocks.js
export async function mockUserEndpoints(page) {
  // Mock for getting all users
  await page.route('**/api/users', route => route.fulfill({
    json: [{ id: 1, name: 'Mocked User' }]
  }));

  // Mock for getting a specific user
  await page.route('**/api/users/1', route => route.fulfill({
    json: { id: 1, name: 'Specific Mocked User', email: '[email protected]' }
  }));
}

Then, in your test file, you can import and use this setup function within a test.beforeEach hook to apply it to all tests in the file.

// tests/user-page.spec.js
import { test, expect } from '@playwright/test';
import { mockUserEndpoints } from './mocks/user-mocks';

test.beforeEach(async ({ page }) => {
  // Set up all user-related mocks before each test
  await mockUserEndpoints(page);
});

test('should display user data', async ({ page }) => {
  await page.goto('/users/1');
  await expect(page.getByText('Specific Mocked User')).toBeVisible();
});

Advanced Technique: Using page.routeFromHAR()

HAR (HTTP Archive) is a JSON-formatted file that logs a browser's interaction with a site. You can record a real user session, including all network requests and responses, and save it as a .har file using your browser's developer tools. Playwright can then use this file to mock an entire network environment with a single command: page.routeFromHAR().

This is incredibly powerful for mocking complex applications with dozens of API calls. According to the W3C HAR specification, it captures timing, headers, and content, providing a high-fidelity snapshot.

Workflow:

  1. Open Chrome DevTools, go to the Network tab, and check "Preserve log".
  2. Perform the user flow you want to capture.
  3. Right-click on the network requests and select "Save all as HAR with content". Save the file (e.g., tests/mocks/login-flow.har).
  4. Use it in your test:
import { test, expect } from '@playwright/test';

test.use({ storageState: 'storageState.json' });

test('should replay login flow from HAR', async ({ page }) => {
  // Arrange: Mock all network calls from the HAR file
  await page.routeFromHAR('tests/mocks/login-flow.har', {
    url: '**/api/**', // Only mock API calls from the HAR
    update: false // Fails if a request is not in the HAR
  });

  // Act
  await page.goto('/dashboard');

  // Assert: The page should render correctly using data from the HAR file
  await expect(page.getByText('Welcome, User!')).toBeVisible();
});

The update: true option is particularly useful. When set, Playwright will use the HAR for mocks but also update the HAR file with any new requests it sees, making it easier to maintain your mock data as the application evolves. The Playwright documentation on HAR files provides more detail on this powerful feature.

General Best Practices:

  • Be Explicit, Not Implicit: Your test should clearly state what it's mocking. Avoid overly broad glob patterns (**/*) that might unintentionally intercept requests you didn't mean to mock.

  • Keep Mocks Scoped: Use test.beforeEach to scope mocks to a single test file. Avoid setting up global mocks unless absolutely necessary, as it can lead to confusing behavior and test inter-dependencies, an anti-pattern discussed in many software engineering blogs.

  • Fail on Unhandled Requests: For maximum test isolation, you can configure Playwright to fail any test that makes a real network request. This forces you to be deliberate about what you mock and what you allow.

    // Fail any request that is not mocked
    await page.route('**/*', route => route.abort());
    
    // Then, set up your specific mocks
    await page.route('**/api/users', route => { /* ... */ });
  • Maintain Your Mocks: An API contract is a living document. When the real API changes, your mocks must be updated. Regularly review and update your mock data and HAR files to prevent your tests from diverging from reality.

Beyond E2E: Using Playwright Mock API in Component Tests

The power of the playwright mock api isn't confined to full end-to-end tests. With the introduction of Playwright's component testing capabilities, the same network interception tools can be used to write lightning-fast, isolated tests for your individual UI components (React, Vue, Svelte, etc.).

In component testing, mocking is not just a best practice; it's practically a requirement. The goal is to test the component's rendering, logic, and user interactions in complete isolation, without any external dependencies like a data store or a real API. Since many modern components are responsible for their own data fetching, page.route() is the perfect tool for the job.

Let's consider a simple React component that fetches and displays a user's name.

The React Component (UserProfile.jsx)

import React, { useState, useEffect } from 'react';

export function UserProfile({ userId }) {
  const [user, setUser] = useState(null);
  const [error, setError] = useState(null);

  useEffect(() => {
    fetch(`/api/users/${userId}`)
      .then(res => {
        if (!res.ok) throw new Error('Failed to fetch');
        return res.json();
      })
      .then(data => setUser(data))
      .catch(err => setError(err.message));
  }, [userId]);

  if (error) return <div>{error}</div>;
  if (!user) return <div>Loading...</div>;

  return <h1>Welcome, {user.name}</h1>;
}

The Playwright Component Test (UserProfile.spec.jsx)

import { test, expect } from '@playwright/experimental-ct-react';
import { UserProfile } from './UserProfile';

test('should display user name after successful fetch', async ({ mount, page }) => {
  // Arrange: Mock the API before mounting the component
  await page.route('**/api/users/123', async route => {
    await route.fulfill({ json: { name: 'Component User' } });
  });

  // Act: Mount the component
  const component = await mount(<UserProfile userId="123" />);

  // Assert: Check for the final rendered state
  await expect(component.getByText('Welcome, Component User')).toBeVisible();
});

test('should display error message on fetch failure', async ({ mount, page }) => {
  // Arrange: Mock a network failure
  await page.route('**/api/users/456', async route => {
    await route.abort();
  });

  // Act
  const component = await mount(<UserProfile userId="456" />);

  // Assert
  await expect(component.getByText('Failed to fetch')).toBeVisible();
});

As you can see, the syntax is identical to E2E testing. You use page.route() to set up the desired network condition before you mount the component. This allows you to test every possible state of your component—loading, success, error—with perfect precision and incredible speed, directly aligning with component-driven development principles promoted by frameworks like Storybook and other industry leaders.

The ability to control the network is a superpower in automated testing. By moving beyond a reliance on live, unpredictable backends, you fundamentally change the nature of your test suite from a source of flaky frustration to a pillar of stability and speed. The playwright mock api, centered around the versatile page.route() function, provides all the tools necessary to achieve this control. From fulfilling simple GET requests and simulating errors to replaying entire user sessions with HAR files, you can craft a testing environment that is perfectly tailored to your application's needs. Adopting these techniques will not only lead to more reliable tests and faster CI/CD pipelines but will also empower your team to build and ship higher-quality software with greater confidence. The initial investment in setting up a robust mocking strategy pays dividends throughout the entire software development lifecycle.

What today's top teams are saying about Momentic:

"Momentic makes it 3x faster for our team to write and maintain end to end tests."

- Alex, CTO, GPTZero

"Works for us in prod, super great UX, and incredible velocity and delivery."

- Aditya, CTO, Best Parents

"…it was done running in 14 min, without me needing to do a thing during that time."

- Mike, Eng Manager, Runway

Increase velocity with reliable AI testing.

Run stable, dev-owned tests on every push. No QA bottlenecks.

Ship it

FAQs

Momentic tests are much more reliable than Playwright or Cypress tests because they are not affected by changes in the DOM.

Our customers often build their first tests within five minutes. It's very easy to build tests using the low-code editor. You can also record your actions and turn them into a fully working automated test.

Not even a little bit. As long as you can clearly describe what you want to test, Momentic can get it done.

Yes. You can use Momentic's CLI to run tests anywhere. We support any CI provider that can run Node.js.

Mobile and desktop support is on our roadmap, but we don't have a specific release date yet.

We currently support Chromium and Chrome browsers for tests. Safari and Firefox support is on our roadmap, but we don't have a specific release date yet.

© 2025 Momentic, Inc.
All rights reserved.