API mocking started making sense once UI tests began failing for reasons that weren’t visible in the browser. A button didn’t render because an API was slow. A test failed because the backend response changed overnight. None of those failures were about the UI, but they still broke the test run. Mocking the API removed that dependency and made the tests predictable again.
Playwright makes this practical by allowing network requests to be intercepted and modified at runtime. Instead of waiting on real services, the UI can be tested against specific responses, error states, or edge cases on demand.
Overview
When to Use API Mocking in Playwright Tests
- When backend services are unstable, slow, rate-limited, or not available in test environments
- When tests need deterministic responses (no data drift across runs)
- When validating UI states driven by edge-case responses (empty lists, 500s, retries, partial data)
- When reproducing hard-to-trigger scenarios (timeouts, malformed payloads, feature-flagged responses)
- When running tests in isolated environments where real dependencies are expensive to set up
This guide explains how to mock APIs in Playwright, covers more advanced scenarios, and points out common pitfalls that show up when mocking is used at scale.
What is API Mocking?
API mocking is the process of creating simulated API endpoints that return predefined responses. Instead of calling a live backend, the application interacts with these mocked endpoints. This approach is useful when the backend is incomplete, unstable, or changing.
Mocked APIs can return static data, dynamic data, or error responses based on the needs of a test scenario. They allow teams to verify how the frontend handles various conditions, such as successful requests, validation failures, or server errors, without relying on real servers.
Read More: The Complete API Testing Guide in 2026
Why Mock APIs with Playwright?
Mocking APIs within Playwright gives full control over the requests and responses during testing. The goal is to create a consistent, predictable test environment that enables faster debugging and development, not just to replace the backend temporarily.
The advantages of mocking APIs in Playwright include:
- Full control over responses: Define exact request and response data for each endpoint to make tests predictable and repeatable. This removes uncertainty from backend changes or unstable data.
- Testing before backend readiness: Build and verify frontend components while backend development is still in progress, reducing idle time for teams.
- Improved execution speed: Bypass real network calls so tests run faster and require fewer resources.
- On-demand error scenarios: Simulate specific failures like 500 server errors, authentication issues, or slow responses to confirm the UI handles them correctly.
- Independence from backend stability: Keep tests running even if backend services are down, misconfigured, or undergoing maintenance.
Setting Up Playwright for Mock API Testing
Before mocking APIs, Playwright must be installed and configured in the project. This ensures the required browser drivers, dependencies, and test runner are ready. The setup process involves:
Use npm or yarn to install Playwright along with its browsers:
npm install @playwright/test npx playwright install
2. Creating a test file
Store tests in a tests directory or any preferred location. Import the Playwright test library at the top of the file:
const { test, expect } = require('@playwright/test');3. Configuring the test runner
Playwright uses a configuration file (playwright.config.js or playwright.config.ts) to define test settings such as timeouts, base URLs, and browser types. This ensures all tests run under the same conditions.
4. Preparing the application URL
Ensure the test points to the correct environment, whether it is a local development server or a staging environment. For API mocking, the target environment should allow interception of requests without authentication conflicts.
Once this setup is complete, you can start intercepting and modifying API calls directly within Playwright scripts.
Read More: How to start with Playwright Debugging?
How to Mock API Requests with Playwright
Playwright controls network traffic through page.route. Tests can intercept a request, choose the response, and then verify the UI behavior. The workflow has four parts: intercept, fulfill, match precisely, and assert. Short snippets below explain each step, followed by a compact end‑to‑end example.
1. Intercept the request
Target the endpoint that the test needs to own. Use a specific pattern for stability.
await page.route('**/api/users', route => {
// Will fulfill in step 2
});2. Define the mock response
Use route.fulfill to return status, headers, and body. Keep static data for predictability. Generate data from query params or request body for realistic behavior.
await page.route('**/api/users', async route => {
const url = new URL(route.request().url());
const pageParam = url.searchParams.get('page');
if (!pageParam) {
return route.fulfill({
status: 200,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify([
{ id: 1, name: 'John Doe' },
{ id: 2, name: 'Jane Smith' }
])
});
}
const base = (Number(pageParam) - 1) * 2;
return route.fulfill({
status: 200,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify([
{ id: base + 1, name: `User ${base + 1}` },
{ id: base + 2, name: `User ${base + 2}` }
])
});
});3. Match request patterns
Keep a broad pass‑through for unrelated calls. Apply a precise pattern to the endpoint under test. This avoids accidental interception.
await page.route('**/api/**', route => route.continue()); // allow the rest
await page.route('**/api/users', route => { /* fulfill here */ });4. Run the test with mocked data
Trigger the UI action that calls the API. Assert on the rendered output, loading behavior, or error messages.
await page.click('#load-default');
await expect(page.locator('#list li')).toHaveCount(2);
await expect(page.locator('#list')).toContainText('1: John Doe');Example: Interception, fulfillment, and verification in one test
The example below shows a broad route for general API traffic, a targeted route with static and dynamic responses, a simple UI that calls the API, and assertions that confirm the UI displays the mocked payloads.
// tests/mock-api.spec.js
const { test, expect } = require('@playwright/test');
test('mock API with static and dynamic responses', async ({ page }) => {
// Broad pass-through for other API calls
await page.route('**/api/**', route => route.continue());
// Targeted interception for the users endpoint
await page.route('**/api/users', async route => {
const url = new URL(route.request().url());
const pageParam = url.searchParams.get('page');
// Static response when no page query is present
if (!pageParam) {
return route.fulfill({
status: 200,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify([
{ id: 1, name: 'John Doe' },
{ id: 2, name: 'Jane Smith' }
])
});
}
// Dynamic response driven by the page query
const base = (Number(pageParam) - 1) * 2;
const users = [
{ id: base + 1, name: `User ${base + 1}` },
{ id: base + 2, name: `User ${base + 2}` }
];
return route.fulfill({
status: 200,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(users)
});
});
// Minimal UI that calls the mocked API
await page.setContent(`
<div>
<button id="load-default">Load default</button>
<button id="load-page-2">Load page 2</button>
<ul id="list"></ul>
<script>
async function load(url) {
const res = await fetch(url);
const data = await res.json();
const list = document.getElementById('list');
list.innerHTML = data.map(u => '<li>' + u.id + ': ' + u.name + '</li>').join('');
}
document.getElementById('load-default').onclick = () => load('/api/users');
document.getElementById('load-page-2').onclick = () => load('/api/users?page=2');
</script>
</div>
`);
// Assertions: static response
await page.click('#load-default');
await expect(page.locator('#list li')).toHaveCount(2);
await expect(page.locator('#list')).toContainText('1: John Doe');
await expect(page.locator('#list')).toContainText('2: Jane Smith');
// Assertions: dynamic response for page=2
await page.click('#load-page-2');
await expect(page.locator('#list li')).toHaveCount(2);
await expect(page.locator('#list')).toContainText('3: User 3');
await expect(page.locator('#list')).toContainText('4: User 4');
});Key takeaways:
- Control the exact response with the route.fulfill: status, headers, body.
- Use precise patterns for the endpoint under test: **/api/users.
- Keep a broad pass‑through for everything else: **/api/**.
- Validate the UI against the expected DOM output for both static and dynamic cases.
Requestly simplifies API mocking in Playwright by providing a visual way to intercept, modify, and mock network requests without writing complex routing logic. It’s especially useful when teams need quick experiments, response overrides, or environment-specific mocks while keeping Playwright tests clean and focused on UI behavior.
Advanced API Mocking Techniques in Playwright
Once basic interception is in place, certain situations call for more control, visibility, or flexibility. The techniques below build on the fundamentals to handle real-world testing challenges.
1. Apply routes at the right scope
Not all mocks should apply globally. Some scenarios require every page in the test to share the same mocks, such as a logged-in state across tabs. Others require page-specific mocks to avoid affecting unrelated tests in the same run. Choosing the correct scope ensures your mocks are active only where they are needed.
// Context-level: applies to every page in this context
const context = await browser.newContext();
await context.route('**/api/**', route => route.continue());
// Page-level: override a single endpoint in one test
const page = await context.newPage();
await page.route('**/api/users', route => route.fulfill({ /* ... */ }));2. Control order and specificity:
Playwright uses the most recently added route that matches a request. Broad “catch-all” routes should be registered first, followed by more specific routes. This ensures targeted mocks are not overridden.
await page.route('**/api/**', r => r.continue()); // broad
await page.route('**/api/users', r => r.fulfill({ /*...*/ })); // specific
await page.unroute('**/api/users'); // cleanup when done
Modify requests instead of replacing responses:
Forward the request while changing headers or payload. Useful for auth headers, A/B flags, or proxying to a different backend.
await page.route('**/api/orders', async route => {
const headers = { ...route.request().headers(), 'x-test-flag': 'A' };
await route.continue({ headers });
});3. Abort to simulate transport failures
Sometimes you still want the request to reach the real backend, but with slight modifications. You can change headers, query parameters, or the request body before passing it on.
await page.route('**/api/payments', route => route.abort('failed')); // or 'timedout'4. Introduce latency deterministically
Some scenarios require testing how the UI behaves when a request cannot reach the backend at all. Instead of returning an error response, you can abort the request to mimic network outages or connectivity loss.
await page.route('**/api/search', async route => {
await new Promise(r => setTimeout(r, 800)); // 800 ms delay
await route.fulfill({ status: 200, body: JSON.stringify({ results: [] }) });
});5. Mock file‑based with HAR
To verify loading indicators, timeouts, or race condition handling, you can delay the mock response before fulfilling it. This simulates slower network conditions or heavy backend processing.
// Replays responses from a HAR file
await context.routeFromHAR('fixtures/app.har', { url: '**/api/**', notFound: 'fallback' });
// Add targeted overrides on top if needed
await context.route('**/api/feature-flags', r => r.fulfill({ body: '{"beta":true}' }));6. Handle GraphQL cleanly
When an API has many endpoints or complex payloads, recording real network traffic and replaying it during tests can be faster than building each mock manually. Playwright supports loading a HAR file and matching requests against it.
await page.route('**/graphql', async route => {
const { operationName, variables } = await route.request().postDataJSON();
if (operationName === 'GetUser') {
return route.fulfill({ body: JSON.stringify({ data: { user: { id: 'u1', name: 'Ava' } } }) });
}
if (operationName === 'Search' && variables.query === 'shoes') {
return route.fulfill({ body: JSON.stringify({ data: { results: [{ id: 'p1', title: 'Runner' }] } }) });
}
return route.continue(); // default
});7. Keep mocks observable
Since GraphQL routes all requests through a single endpoint, the request body must be inspected to decide which mock to return. This allows mocking specific queries or mutations while letting others pass through.
await page.route('**/api/users', r => r.fulfill({
status: 200,
headers: { 'Content-Type': 'application/json', 'x-mocked': 'true' },
body: JSON.stringify([{ id: 1, name: 'Mocked User' }])
}));8. Make mocks observable
When troubleshooting test runs, it is useful to know which responses came from mocks instead of the live backend. Adding a custom header or marker in the mock makes it easy to spot these responses in browser DevTools or network logs.
await page.route('**/api/users', r => r.fulfill({
status: 200,
headers: { 'Content-Type': 'application/json', 'x-mocked': 'true' },
body: JSON.stringify([{ id: 1, name: 'Mocked User' }])
}));Read More: Async/Await in Playwright
Mocking Dynamic Data with Playwright for Realistic Testing
Static responses work for basic rendering checks, but they fall short when an application relies on changing backend data. These techniques show different ways to make mocks act like a real API, allowing the UI to read, write, and interact with evolving data.
1. Maintain in-memory state
To simulate a backend that changes over time, keep a data store in memory during the test. Handle GET requests by returning the current state, and update it in response to POST, PUT, or DELETE calls.
const db = { users: [{ id: 1, name: 'Ava' }] };
let nextId = 2;
await page.route('**/api/users', async route => {
const req = route.request();
if (req.method() === 'GET') {
return route.fulfill({ body: JSON.stringify(db.users) });
}
if (req.method() === 'POST') {
const { name } = await req.postDataJSON();
const user = { id: nextId++, name };
db.users.push(user);
return route.fulfill({ status: 201, body: JSON.stringify(user) });
}
return route.continue();
});Also Read: HTTP Methods: GET vs POST vs PUSH
2. Implement filtering, sorting, and pagination
If the real API applies query parameters to filter, sort, or paginate results, your mocks should do the same. This ensures that UI controls like search boxes and sort dropdowns are tested against realistic behavior.
const sortBy = 'name:asc';
const [field, dir] = sortBy.split(':');
const sorted = [...db.users].sort((a, b) =>
dir === 'asc' ? a[field].localeCompare(b[field]) : b[field].localeCompare(a[field])
);3. Enforce authentication or authorization
Some endpoints require specific tokens or permissions. You can replicate this by checking request headers and returning a 403 or 401 if the request is not authorized.
await page.route('**/api/admin/**', async route => {
const token = route.request().headers()['authorization'];
if (token !== 'Bearer test-admin') {
return route.fulfill({ status: 403, body: '{"error":"forbidden"}' });
}
return route.continue();
});4. Generate server-side fields
APIs often add metadata such as IDs, timestamps, or version numbers to responses. Including these fields in your mocks allows the UI to behave exactly as it would with production data.
const now = new Date().toISOString();
const item = { id: crypto.randomUUID(), createdAt: now, updatedAt: now };5. Model errors consistently
The shape of error responses matters for UI validation. If the real API sends validation errors in a certain format, your mocks should use the same structure so the UI handles them correctly.
return route.fulfill({
status: 422,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ errors: [{ field: 'name', code: 'too_short', min: 3 }] })
});6. Simulate network variability
To test how the UI behaves on slow or inconsistent networks, you can delay responses or introduce jitter in response times.
const delay = ms => new Promise(r => setTimeout(r, ms));
await page.route('**/api/slow-reports', async route => {
await delay(1200 + Math.floor(Math.random() * 300)); // jitter
await route.fulfill({ status: 200, body: '{"ok":true}' });
});7. Combine recorded and dynamic mocks
Some endpoints may be fine replayed from a static HAR file, while others need dynamic behavior. You can use both approaches in the same test.
await context.routeFromHAR('fixtures/baseline.har', { url: '**/api/**', notFound: 'fallback' });
await context.route('**/api/clock', r => r.fulfill({ body: JSON.stringify({ now: '2030-01-01T00:00:00Z' }) }));Mocking vs Stubbing vs Real APIs in Playwright
API-dependent UI tests can behave very differently depending on how backend interactions are handled. In Playwright, teams typically choose between mocking, stubbing, or calling real APIs based on what the test is trying to validate. Each approach serves a distinct purpose and comes with clear trade-offs in reliability, coverage, and maintenance.
The table below compares mocking, stubbing, and real API usage in Playwright to help decide when each approach fits best.
| Aspect | Mocking APIs | Stubbing APIs | Real APIs |
|---|---|---|---|
| What it does | Intercepts requests and returns fully controlled responses | Replaces specific API calls with simple fixed responses | Sends requests to actual backend services |
| Level of control | Full control over status codes, payloads, delays, and errors | Limited control, usually static responses | No control, depends on backend behavior |
| Test stability | Very high, results are deterministic | High, but limited to predefined cases | Lower, affected by data changes and service health |
| Setup complexity | Moderate, requires routing and response logic | Low, simple response replacement | Low setup, but high dependency management |
| Best for | UI behavior, edge cases, error handling, isolated tests | Basic UI flows with minimal backend dependency | End-to-end integration and contract validation |
| Risk | Can hide backend issues if overused | Can oversimplify real scenarios | Can cause flaky tests due to external factors |
| Execution speed | Fast | Fast | Slower, dependent on network and service latency |
Used correctly, mocking and stubbing improve reliability and speed, while real APIs provide confidence that the system works as a whole. A balanced Playwright test strategy usually combines all three rather than relying on one approach exclusively.
When to Use API Mocking in Playwright Tests
API mocking is most effective when UI behavior needs to be validated without depending on live backend services. In Playwright tests, mocking should be used in the following scenarios:
- Backend services are unstable or unavailable: Mocking removes dependency on services that are still under development, rate-limited, or frequently down.
- Tests require deterministic outcomes: Controlled responses ensure the same data is returned on every run, eliminating failures caused by data drift.
- Validating edge cases and error states: Mocking makes it easy to test 404s, 500s, empty responses, partial payloads, and timeout scenarios that are hard to reproduce with real APIs.
- Speed and parallel execution matter: Mocked responses return instantly, keeping test suites fast and stable during parallel runs.
- Testing UI logic in isolation: Mocking allows the UI to be tested independently from backend logic, focusing purely on rendering, state changes, and user interactions.
- Reproducing hard-to-trigger scenarios: Feature flags, rare API failures, or conditional responses can be simulated reliably using mocks.
Used in these cases, API mocking helps Playwright tests remain stable, fast, and focused on UI correctness rather than backend behavior.
When Not to Mock APIs in Playwright
API mocking is not suitable for every test scenario. In Playwright, mocking should be avoided when the goal is to validate real system behavior or catch backend-related issues. The following cases are better served by real API calls:
- Validating end-to-end integrations: Authentication, payment flows, and critical business workflows require real backend interaction to ensure systems work together correctly.
- Catching backend regressions: Mocked responses can hide breaking API changes, schema mismatches, or logic errors that only appear when real services are involved.
- Testing production-like behavior: When accuracy matters more than speed, real APIs provide confidence that the application behaves correctly under real conditions.
- Maintaining contract reliability: If API contracts change frequently, mocks become expensive to maintain and can quickly fall out of sync with actual responses.
- Smoke and release validation tests: A small set of tests should always run against real APIs to act as a final confidence check before release.
Avoiding mocks in these scenarios ensures Playwright tests continue to reflect real application behavior instead of simulated conditions.
How Requestly Enhances API Mocking with Playwright
Requestly by BrowserStack is an HTTP interception and API mocking tool available as a browser extension or desktop app. It offers a visual interface for creating, modifying, and sharing mock rules without touching Playwright code.
This makes Requestly useful for quick setup during development, for team-wide mock sharing, or for scenarios where mocks need to be applied outside of automated test scripts.
Key features that help with Playwright API mocking:
- Modify API Responses: Change response bodies for specific endpoints to test different UI states.
- Create Mock Endpoints: Host mock APIs that Playwright tests can call, ensuring stable and reusable mock environments.
- Modify HTTP Status Code: Return custom status codes like 404 or 500 to test error handling.
- Supports GraphQL API Overrides: Target GraphQL requests by operation name or variables to control which queries are mocked.
- Delay Request: Introduce latency to simulate slow network conditions and verify loading states.
- Block Network Requests: Prevent certain calls from completing to simulate outages or remove noise like analytics requests.
Common Challenges with API Mocking in Playwright
Mocking gives control, yet it introduces new failure modes. The points below explain what typically goes wrong and how to avoid it.
- Over‑broad routes: Wildcard patterns capture extra traffic and change behavior unintentionally. Prefer exact patterns per endpoint and add pass‑through routes for the rest.
- Route registration timing: Routes added after navigation miss early requests. Register routes before the page.goto or use context‑level routes when setup is global.
- Most recent match confusion: Playwright uses the last matching route. Add broad pass‑through first, then specific mocks. Remove routes when a test no longer needs them.
- State leakage between tests: In‑memory stores and active routes bleed across cases. Create a fresh browser context per test and call unroute during teardown.
- Mock drift from the API contract: Shapes, HTTP status codes, and headers deviate from the real spec. Base mocks on the schema or examples and keep them versioned with the app.
- Auth and CORS gaps: Mocked calls may miss auth headers or CORS headers. Set Authorization on requests you continue, and include Access‑Control‑Allow‑Origin on fulfilled mocks when the app requires it.
- HTTP Archive (HAR) replay pitfalls: Recorded fixtures go stale or fail on URL mismatches. Re‑record after API changes and use notFound: ‘fallback’ with targeted overrides.
Best Practices to Mock APIs with Playwright
API mocking in Playwright can be highly effective when it is structured, predictable, and aligned with the real backend’s behavior. The following practices help maintain reliable tests and reduce maintenance overhead.
- Use precise route matching: Match requests with exact URL patterns whenever possible. Avoid overly broad wildcards like **/api/** unless followed by targeted routes for critical endpoints.
- Register routes before navigation: Set up mocks before calling page.goto to ensure early requests are intercepted. For global mocks, register routes at the browser context level.
- Separate mock logic from test logic: Store mock definitions in helper functions or dedicated files. This keeps tests readable and makes mocks easier to update.
- Version mocks alongside API changes: Keep mock data in sync with backend schemas. Update mock files when API contracts change to avoid false positives.
- Use dynamic mocks where needed: Return different responses based on query parameters, request bodies, or test variables to simulate real-world variations.
- Simulate realistic delays and failures: Introduce latency, timeouts, or aborted requests to test error handling and loading states.
- Combine mocking with live calls selectively: Allow stable, non-critical endpoints to use the live backend and mock only those essential to the test.
Read More: API Best Practices: A Complete Guide
Conclusion
Mocking APIs with Playwright gives full control over test conditions, enabling predictable, repeatable, and faster UI testing. By intercepting requests and crafting precise responses, teams can test features early, simulate complex scenarios, and validate error handling without depending on backend availability.
Requestly enables API mocking without modifying test code. It supports overriding responses, changing status codes, adding latency, and blocking requests. These capabilities allow teams to replicate production scenarios, investigate defects, and verify fixes efficiently during development or manual testing.