New: Playwright Modern Automation Engineer program - enrolment opening soon
API Testing

Hybrid UI + API Testing in Playwright: Create Data via API, Verify in the UI

Make Playwright end-to-end tests faster and more reliable: create test data through the API, check the result in the UI and clean up with fixtures.

By admin · · 4 min read

A typical end-to-end test spends most of its time on setup. Before checking that a customer can see their order, it logs in as an admin, fills in a product form, saves it, logs out and logs in again. Every one of those steps is slow and can fail for reasons that have nothing to do with the feature under test. Hybrid testing fixes this: use the API for setup and cleanup, and use the browser only for what you actually want to verify.

Why send setup through the API?

Setup through the UISetup through the API
SpeedSeconds per stepMilliseconds per request
ReliabilityDepends on many pages loading correctlyOne request, one clear response
Failure messages"Button not found" on an unrelated pageStatus code and response body
FocusTest checks many features at onceTest checks the feature it is named after

The browser is still essential. It is the only way to confirm that a real user sees the right thing. Hybrid testing simply stops using the browser for work the API can do better.

The pattern: API → UI → API

  1. Arrange with the API: create the user, product or order the test needs.
  2. Act and assert in the UI: perform the user journey and check what the user sees.
  3. Clean up with the API: delete the data so tests stay independent.

A first hybrid test

Playwright Test includes a built-in request fixture, an APIRequestContext that sends HTTP requests without opening a browser. It uses the baseURL from your config, just like page.goto().

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

test('a new product appears in the catalogue', async ({ page, request }) => {
  // Arrange: create the product through the API
  const created = await request.post('/api/products', {
    data: { name: `Test mug ${Date.now()}`, price: 12 },
  });
  await expect(created).toBeOK();
  const product = await created.json();

  // Act + assert: check what a customer sees
  await page.goto('/catalogue');
  await expect(page.getByRole('heading', { name: product.name })).toBeVisible();

  // Clean up
  await request.delete(`/api/products/${product.id}`);
});

toBeOK() checks that the response status is in the 2xx range and prints the response details if it is not, which makes failures much easier to diagnose than a missing element on a later page.

Put setup and cleanup in a fixture

The test above has a weakness: if the assertion fails, the cleanup line never runs. A custom fixture guarantees cleanup and keeps tests short.

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

type Product = { id: number; name: string };

export const test = base.extend<{ product: Product }>({
  product: async ({ request }, use) => {
    const res = await request.post('/api/products', {
      data: { name: `Test mug ${Date.now()}`, price: 12 },
    });
    await expect(res).toBeOK();
    const product: Product = await res.json();

    await use(product);                                   // the test runs here

    await request.delete(`/api/products/${product.id}`);  // always runs afterwards
  },
});

test('a new product appears in the catalogue', async ({ page, product }) => {
  await page.goto('/catalogue');
  await expect(page.getByRole('heading', { name: product.name })).toBeVisible();
});

Any test that asks for product now gets fresh data and automatic cleanup, even when it fails.

Verify UI actions through the API

The pattern also works in reverse. After a user changes something in the browser, confirm that the change was really saved, not just displayed:

await page.getByLabel('Display name').fill('Priya');
await page.getByRole('button', { name: 'Save profile' }).click();
await expect(page.getByText('Profile updated')).toBeVisible();

const profile = await page.request.get('/api/me');
expect((await profile.json()).displayName).toBe('Priya');

page.request shares cookies with the browser context, so the API call is made as the same logged-in user.

Make it safe for parallel runs

Playwright runs tests in parallel by default, so hybrid tests must never fight over the same data:

  • Generate unique values (a timestamp, a random suffix or testInfo.workerIndex) for names and emails.
  • Never depend on records created by another test.
  • Delete what you create, ideally in a fixture.

When not to use the API

Keep at least one test that walks through the real journey entirely in the UI, for example sign-up or checkout. That test proves the screens connect correctly. Hybrid tests then cover the many variations quickly without repeating that journey every time.

Key takeaways

  • Use the API for setup and cleanup, and the browser for what the user sees.
  • Playwright's request fixture and toBeOK() make API steps short and readable.
  • Fixtures guarantee cleanup even when a test fails.
  • page.request lets you verify UI changes as the logged-in user.
  • Unique test data keeps hybrid tests safe in parallel.

Frequently asked questions

Can Playwright do API testing?

Yes. Playwright includes APIRequestContext, available as the request fixture, for sending GET, POST, PUT, PATCH and DELETE requests and asserting on responses. You can use it for pure API tests or combine it with browser tests.

What is a hybrid test in test automation?

A hybrid test combines API calls and UI steps in one test. Typically the API prepares and removes test data, while the UI checks the behaviour a real user experiences.

Do hybrid tests replace end-to-end tests?

No. Keep a small number of full UI journeys to prove the screens work together, and use hybrid tests for the many variations of each feature.

API testing, hybrid workflows and fixtures are covered in Chapter 5 of our Playwright Modern Automation Engineer program. Next, read how to stop hiding flaky tests behind retries.

Weekly testing tips

One email every Friday with Playwright tips, framework patterns and enrolment updates. No spam, unsubscribe anytime.