July 15, 2026
How to Combine API Calls with Playwright Tests
Learn how to combine API calls with Playwright tests to set up data, validate backend behavior, and build faster API and UI tests with Playwright request context.
If you already use Playwright for browser automation, one of the most useful upgrades is to let the test talk to your backend too. That usually means using API calls to create data, seed state, fetch IDs, verify side effects, or clean up after a UI flow. Done well, this gives you faster tests, more stable setup, and better coverage across the boundary where most real bugs live, between the UI and the API.
In practice, the best Playwright suites are rarely UI-only. They mix browser steps with direct requests so the test can prepare exactly the data it needs, then assert that the application behaves correctly from the user’s point of view. That is the core idea behind how to combine API calls with Playwright tests.
Why combine API and UI testing at all?
A browser test is excellent at validating a real user path, but it is usually a poor way to prepare test data. If you need to log in through the UI, click through half a dozen screens, create a record, then finally check the page you care about, the test becomes slower and more brittle than it needs to be.
An API call can often do the setup in a single request.
For example:
- Create a user through the API instead of filling a registration form
- Seed an order, project, or ticket with a known state
- Fetch an auth token without navigating a login page
- Assert a backend response after a browser action has triggered it
- Reset data in teardown without depending on the UI
This is especially valuable when the UI is not the feature under test. If the feature is “a user can see the correct order status after checkout,” then the checkout form might not be worth testing every time. Create the order through the API, open the order page in the browser, and focus the test on what the user sees.
A good mixed test usually spends browser time on the thing users experience, and API time on the things users should never notice, like data setup or cleanup.
Playwright’s API layer, in plain terms
Playwright includes request testing capabilities through its request context. In TypeScript tests, this lets you send HTTP requests without opening a browser page. You can create a separate API client with request.newContext(), make calls, inspect status codes, read JSON, and reuse responses later in the test.
That means you do not need a separate API framework just to set up or verify data in a browser test. You can keep the API interaction close to the UI interaction, which makes the test easier to read and maintain.
The pattern usually looks like this:
- Create or fetch data with an API request
- Store the returned values in variables
- Open the browser page and use that data
- Perform UI actions
- Use the API again to verify the backend state if needed
This is different from pure API testing. In Playwright API testing, the API is often a supporting actor in a larger end-to-end flow, not the whole test.
A simple example, creating test data via API before using the UI
Suppose you have an app where you can create a project through the UI, but creating it through the API is faster and more reliable for setup.
import { test, expect, request } from '@playwright/test';
test('user can view a project created through the API', async ({ page }) => {
const api = await request.newContext({
baseURL: 'https://api.example.com',
extraHTTPHeaders: {
Authorization: `Bearer ${process.env.API_TOKEN}`,
},
});
const createResponse = await api.post(‘/projects’, { data: { name: ‘Playwright API demo’, visibility: ‘private’, }, });
expect(createResponse.ok()).toBeTruthy(); const project = await createResponse.json();
await page.goto(/projects/${project.id});
await expect(page.getByRole(‘heading’, { name: ‘Playwright API demo’ })).toBeVisible();
});
This is one of the cleanest ways to combine API calls with Playwright tests. The API creates exactly the fixture you need, and the browser checks that the app renders it correctly.
A few details matter here:
- Use
baseURLso requests stay short and readable - Keep secrets in environment variables, not hardcoded in tests
- Assert the API response before depending on it in the UI
- Reuse the ID or other returned fields immediately so the test stays self-contained
When to use API setup, and when not to
Not every test should use API setup. If the purpose of the test is to validate the user journey from the first click to the last confirmation, then bypassing the form may remove important coverage.
A useful rule is:
- Use the UI when the interaction itself matters
- Use the API when the setup or verification does not need browser coverage
Examples where API setup is a good fit:
- Creating a tenant, account, or workspace before a dashboard test
- Preparing an item with a rare state, like “archived”, “failed”, or “pending review”
- Seeding feature flags, roles, or permissions
- Creating a checkout session before testing a payment status page
Examples where UI setup is still important:
- Validating a signup flow
- Testing form validation rules
- Checking accessibility of a multi-step wizard
- Verifying front-end behavior around dynamic input masks or autocompletion
In other words, do not remove UI steps just because an API exists. Remove UI setup when it does not add useful signal.
Using API calls to authenticate faster
Login flows are a classic place where API and UI tests Playwright patterns help. UI login can be useful for smoke coverage, but for many tests, it is just overhead.
If your app issues session cookies or bearer tokens through an auth endpoint, you can authenticate once, store the browser state, and reuse it.
import { test, expect, request } from '@playwright/test';
test('opens dashboard as an authenticated user', async ({ page }) => {
const api = await request.newContext({ baseURL: 'https://api.example.com' });
const login = await api.post(‘/auth/login’, { data: { email: process.env.TEST_USER_EMAIL, password: process.env.TEST_USER_PASSWORD, }, });
expect(login.ok()).toBeTruthy(); const { token } = await login.json();
await page.addInitScript(value => { window.localStorage.setItem(‘auth_token’, value); }, token);
await page.goto(‘/dashboard’); await expect(page.getByText(‘Welcome back’)).toBeVisible(); });
This example uses local storage for simplicity, but the same idea works for cookies, headers, or storage state files.
If you reuse authentication across many tests, Playwright’s storageState can be a better fit than logging in through the API in every test. Still, direct API login is often great for short-lived setup in a single test.
Verifying backend side effects after UI actions
Combining API calls with Playwright tests is not only about setup. It is also useful after a UI action triggers backend work.
Imagine a user clicks “Save” in the UI. The page may show success before the backend work is truly complete, especially if the app uses async jobs or eventual consistency. If the application exposes a stable API, you can query the backend after the click to verify the side effect.
import { test, expect, request } from '@playwright/test';
test('saving a profile updates the backend record', async ({ page }) => {
const api = await request.newContext({ baseURL: 'https://api.example.com' });
await page.goto(‘/settings/profile’); await page.getByLabel(‘Display name’).fill(‘Ada Testing’); await page.getByRole(‘button’, { name: ‘Save changes’ }).click();
await expect(page.getByText(‘Profile updated’)).toBeVisible();
const response = await api.get(‘/me’); expect(response.ok()).toBeTruthy(); const me = await response.json(); expect(me.displayName).toBe(‘Ada Testing’); });
This pattern is useful when the UI confirmation is not enough, and you want to make sure the backend state matches what the user did.
If a click changes persistent data, the UI assertion tells you the flow worked, the API assertion tells you the system actually changed.
Chaining requests inside a test
Sometimes one API call is only the beginning. You may create one resource, extract its ID, update it, then open the page in the browser.
That chain can be very readable if you keep the requests focused.
import { test, expect, request } from '@playwright/test';
test('creates and publishes a draft article', async ({ page }) => {
const api = await request.newContext({ baseURL: 'https://api.example.com' });
const draftRes = await api.post(‘/articles’, { data: { title: ‘API and UI testing’, status: ‘draft’ }, }); const draft = await draftRes.json();
const publishRes = await api.post(/articles/${draft.id}/publish);
expect(publishRes.ok()).toBeTruthy();
await page.goto(/articles/${draft.id});
await expect(page.getByText(‘Published’)).toBeVisible();
});
This is a good example of a mixed workflow where the API prepares the state and the UI verifies the end result. It is also a reminder to keep your test focused on one business story. If the test starts doing five unrelated API calls, it will become hard to diagnose when it fails.
Useful patterns for Playwright request context
A few request-context habits make mixed API and UI tests much easier to maintain.
1. Create a helper for API clients
If many tests need the same headers or base URL, wrap the setup.
import { request } from '@playwright/test';
export async function createApiClient() {
return request.newContext({
baseURL: process.env.API_BASE_URL,
extraHTTPHeaders: {
Authorization: Bearer ${process.env.API_TOKEN},
},
});
}
2. Extract only the fields you need
Do not pass a giant response object around unless you really need it. Pull out the IDs or display fields the test depends on.
3. Assert response shape early
If the API response is wrong, fail before the browser steps. That makes debugging much faster.
4. Clean up when the test creates persistent data
Create a delete request in teardown if the environment does not reset automatically.
typescript
const created = await api.post('/projects', { data: { name: 'temp-project' } });
const project = await created.json();
try {
await page.goto(/projects/${project.id});
await expect(page.getByText(‘temp-project’)).toBeVisible();
} finally {
await api.delete(/projects/${project.id});
}
Common mistakes when combining API and browser steps
Mixed tests are powerful, but they are easy to abuse. A few mistakes show up again and again.
Making tests too integration-heavy
If one test covers API auth, database seeding, browser navigation, and email sending, failures will be difficult to interpret. Keep the API usage narrow and purposeful.
Skipping authorization realism
A test that uses an admin token for everything may hide permission bugs. If your app has role-based access, make sure at least some tests use realistic user roles and tokens.
Depending on unstable endpoints
If the API endpoint itself is flaky or highly volatile, your browser test will inherit that instability. In that case, you may want a separate contract test or service test for the API, and keep the Playwright test focused on UI behavior.
Overusing fixed waits
After API calls, do not assume the UI will update instantly. Use Playwright assertions that wait for the right state, instead of sleeping.
typescript
await expect(page.getByText('Processing complete')).toBeVisible();
Reusing the same test data across parallel runs
If multiple workers hit the same account or record, tests can interfere with each other. Generate unique names, emails, or IDs when needed.
Handling async backend behavior
Some UI actions trigger background jobs, webhooks, or cache refreshes. In those cases, the browser may show a success state before the backend record becomes queryable.
A reliable pattern is to poll the API until the expected state appears, but keep the polling bounded.
typescript
async function waitForStatus(api, id: string, expected: string) {
for (let i = 0; i < 10; i++) {
const res = await api.get(`/jobs/${id}`);
const body = await res.json();
if (body.status === expected) return body;
await new Promise(r => setTimeout(r, 1000));
}
throw new Error(`Timed out waiting for status ${expected}`);
}
This is often better than waiting in the UI, especially when the backend is the source of truth. Still, keep the timeout realistic and the error messages clear.
API and UI tests Playwright teams should agree on a testing strategy
If you are doing this in a team setting, the real question is not whether Playwright can call APIs. It can. The real question is how much of your validation belongs in browser tests versus service tests.
A practical split looks like this:
- Unit tests, validate business logic in isolation
- API tests, validate request and response behavior at the service boundary
- Playwright tests, validate user journeys and critical cross-layer flows
- A small number of mixed API and UI tests, validate fast setup plus real browser behavior
This layered approach lines up well with the general idea of test automation in continuous integration, where you want fast feedback for core logic, and a smaller number of broader tests for end-to-end confidence.
If you are defining a broader API strategy, it can also help to document your service contracts with the OpenAPI Specification. Even if your Playwright tests do not consume the schema directly, it makes it much easier to understand what can be created, updated, or queried from the backend.
A practical CI example
Mixed tests work best when they fit cleanly into CI. That means environment variables for secrets, stable base URLs, and browser projects that are easy to debug.
name: playwright
on: push: branches: [main] pull_request:
jobs: test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: 20 - run: npm ci - run: npx playwright install –with-deps - run: npx playwright test env: API_BASE_URL: https://api.example.com API_TOKEN: $
In CI, the biggest risk is usually not the API code itself, it is brittle environment setup. Keep the test data strategy simple, and avoid depending on manually created records.
When Endtest, an agentic AI test automation platform, may be a simpler option
If you want the ability to mix browser steps with API requests but do not want to own a Playwright codebase, Endtest is worth a look. It supports API requests inside broader end-to-end workflows, so you can set up data, assert responses, and continue with browser steps in the same test, without writing framework code.
That matters when the team wants shared ownership. In a no-code setup, non-framework specialists can still build and maintain mixed UI plus API flows, and the test remains readable as a sequence of steps instead of a pile of helper functions. For teams comparing options, the Endtest vs Playwright page is useful context, especially if the bottleneck is not browser automation itself but the cost of maintaining a code-first framework.
How I decide between pure Playwright, mixed tests, and a no-code platform
I usually pick based on three questions:
- Does the test need code-level flexibility, custom data logic, or tight integration with the rest of the engineering stack?
- Is the main value in browser behavior, or in quickly combining multiple layers of the system?
- Who needs to own the test long term, developers only, or a broader QA and product group?
If the answer to the first two is yes, Playwright plus API calls is a strong fit. If the third question matters a lot, a platform like Endtest can be a simpler operational choice because it reduces framework ownership while still supporting API steps in the same flow.
A few rules I follow in real projects
Here is the short version of the approach I use most often:
- Use API calls for setup, not for everything
- Keep the browser assertions user-facing
- Fail fast on bad responses before opening the page
- Reuse auth intelligently, but do not hide permission problems
- Make test data unique per run when parallel execution is enabled
- Clean up persistent data if the environment does not reset it
The goal is not to make every test shorter. The goal is to make each test more deliberate, more stable, and easier to debug.
Final thought
The best reason to combine API calls with Playwright tests is not speed alone. Speed helps, but the bigger win is control. You can prepare exactly the state you need, verify the system from both sides of the contract, and keep the browser focused on what only a browser can prove.
That is why mixed API and UI tests Playwright teams write tend to age better than pure UI scripts that do everything through clicks. The API handles setup and verification where it is strongest, the browser handles the user journey, and the test stays closer to the real behavior of the product.
If you are just getting started, begin with one or two tests that create data through the API and validate it in the UI. Once that pattern feels natural, you will likely find several other brittle flows that can be simplified the same way.