The fastest, most reliable end-to-end tests use the API for setup and checks and the UI only for what the user actually does. This guide shows UI + API integration testing in Playwright: create data with the request fixture, verify it in the browser, confirm UI actions in the backend, and clean up.
API Testing, Then UI + API Combined
Time: ~25 minutes. You will test a REST API with no browser, then use the API to make a UI test faster and stronger. Uses public demo APIs so you can run it immediately.
Goal
Passing API CRUD tests, plus one test that shows the API+UI pattern.
Part A — Pure API tests
Step 1 — Create an API test file
Create tests/api.spec.ts:
import { test, expect } from '@playwright/test';const BASE = 'https://reqres.in/api';test('GET a list of users returns data', async ({ request }) => { const res = await request.get(`${BASE}/users`, { params: { page: 2 } }); expect(res.status()).toBe(200); const body = await res.json(); expect(body.data.length).toBeGreaterThan(0);});test('POST creates a user and returns 201 with an id', async ({ request }) => { const res = await request.post(`${BASE}/users`, { data: { name: 'Asha', job: 'SDET' }, }); expect(res.status()).toBe(201); const body = await res.json(); expect(body.id).toBeTruthy(); // server returned an id expect(body.name).toBe('Asha');});
Step 2 — Run the API tests (notice: no browser opens)
npx playwright test api
✅ 2 passed — and they run in well under a second because there is no UI. This is why API tests are your fast, stable layer.
Step 3 — Add an UPDATE and DELETE (full CRUD)
Add to tests/api.spec.ts:
test('PUT updates a user', async ({ request }) => { const res = await request.put(`${BASE}/users/2`, { data: { job: 'Lead SDET' } }); expect(res.status()).toBe(200); expect((await res.json()).job).toBe('Lead SDET');});test('DELETE removes a user', async ({ request }) => { const res = await request.delete(`${BASE}/users/2`); expect(res.status()).toBe(204); // 204 = success, no content});
npx playwright test api
✅ 4 passed. You’ve exercised GET, POST, PUT, DELETE and asserted both status and body.
Part B — The API + UI pattern
The idea: do slow setup (or verification) through the API, and only exercise the feature under test through the UI. Here’s the pattern demonstrated end-to-end.
Step 4 — Create a combined test
Create tests/ui-api.spec.ts:
import { test, expect } from '@playwright/test';test('use the API to fetch data, then verify the UI renders it', async ({ page, request }) => { // 1) API: get a known user quickly (no clicking through screens) const res = await request.get('https://reqres.in/api/users/2'); expect(res.status()).toBe(200); const user = (await res.json()).data; // { id, email, first_name, ... } // 2) UI: (demo) show that we can drive a page and assert something real await page.goto('https://playwright.dev/'); await expect(page).toHaveTitle(/Playwright/); // 3) Assert the API gave us the shape we expected expect(user.email).toContain('@'); expect(user.first_name).toBeTruthy();});
In a real app, step 1 would seed a product/order via your own API, step 2 would open the page that shows it, and step 3 would assert it renders — turning a 20-second UI setup into a 200ms request.
Step 5 — Run it
npx playwright test ui-api
✅ 1 passed.
Step 6 — Understand when to use which
- Backend logic / edge cases / setup / cleanup → API (fast, stable).
- The actual user journey under test → UI.
- Critical flows → verify at BOTH layers (UI shows success AND the API/DB confirms it persisted).
What you just learned
- How to test a REST API with the request fixture (GET/POST/PUT/DELETE, status + body).
- Why API tests are your fast, stable layer.
- The API-for-setup, UI-for-journey pattern that makes suites faster and stronger.
Next
Walkthrough 7 makes login happen once for the whole suite using storageState.
API Testing
Level: L3–L4. Playwright has a full HTTP client built in.
What is it?
Testing REST APIs directly (no browser) using Playwright’s request fixture / APIRequestContext: GET/POST/PUT/PATCH/DELETE, headers, params, bodies, and response validation.
Why do we need it?
APIs are faster and more stable to test than UI. Use them to validate backend logic, seed/clean data for UI tests, and cover cases hard to reach through the UI.
How does it work?
The request fixture sends real HTTP requests and returns a response object with status(), ok(), json(), headers(). Assert on those.
Syntax
test('GET users', async ({ request }) => { const res = await request.get('/api/users', { params: { page: 2 } }); expect(res.status()).toBe(200); const body = await res.json(); expect(body.data.length).toBeGreaterThan(0);});
All verbs:
await request.post('/api/users', { data: { name: 'A' }, headers: { Authorization: `Bearer ${t}` } });await request.put('/api/users/1', { data: { name: 'B' } });await request.patch('/api/users/1', { data: { name: 'C' } });await request.delete('/api/users/1');
Basic Example
const res = await request.get('https://reqres.in/api/users/2');expect(res.ok()).toBeTruthy();
Practical Example — create, validate, chain, delete
test('user CRUD', async ({ request }) => { // create const create = await request.post('/api/users', { data: { name: 'Asha', job: 'SDET' } }); expect(create.status()).toBe(201); const { id } = await create.json(); // read (chain uses id) const read = await request.get(`/api/users/${id}`); expect(read.status()).toBe(200); expect((await read.json()).name).toBe('Asha'); // update const upd = await request.put(`/api/users/${id}`, { data: { job: 'Lead SDET' } }); expect(upd.status()).toBe(200); // delete const del = await request.delete(`/api/users/${id}`); expect(del.status()).toBe(204);});
Line-by-Line Explanation
- Create returns 201 and an id → captured for chaining.
- Read/Update/Delete reuse that id, asserting status and body at each step (API chaining).
- One test validates the full resource lifecycle at the backend level.
Schema validation (optional, powerful)
import Ajv from 'ajv';const ajv = new Ajv();const validate = ajv.compile({ type: 'object', required: ['id','name'], properties: { id: { type: ['number','string'] }, name: { type: 'string' } } });expect(validate(await read.json())).toBe(true);
Common Mistakes
- Asserting only status, never the body.
- Hard-coding auth tokens (use env/fixtures).
- Not cleaning up created resources.
- Ignoring response schema (silent contract drift).
Best Practices
- Validate status and body (and schema for critical endpoints).
- Centralize base URL/auth in config/fixtures.
- Use API to seed/clean data for UI tests (fast, reliable).
- Chain via captured ids; clean up in teardown.
Interview Questions
- Q: How does Playwright do API testing? A: Via the request fixture/APIRequestContext — real HTTP with response assertions.
- Q: Why test APIs separately from UI? A: Faster, more stable, better coverage of backend logic and edge cases.
- Q: What is API chaining? A: Using output from one request (e.g., an id) as input to the next.
- Q: How to validate a response contract? A: JSON schema validation (e.g., Ajv) on the body.
Practice Exercise
Against a public API (e.g., reqres.in): write GET/POST/PUT/DELETE tests, chain a created id through the flow, and add one JSON-schema validation.
Real-World Scenario
A UI checkout test needs a product in a specific state. Instead of clicking through admin screens, the test seeds it via one API POST in beforeAll — cutting setup from 20s of UI steps to a 200ms request and removing a flaky dependency.
UI + API Integration Testing
Level: L3–L4.
What is it?
Combining API and UI in one test: use the API for setup/verification and the UI for the user journey (and vice versa).
Why do we need it?
It makes tests faster and more reliable (skip slow UI setup), and lets you verify that UI actions produced correct backend state — true end-to-end confidence.
How does it work?
Both request and page fixtures are available in the same test. Seed/verify via API; interact/assert via UI.
Patterns
A) API seeds data → UI validates it renders correctly
B) UI creates data → API validates the backend stored it
C) API cleans up → keeps environment tidy
Basic Example (API seed → UI check)
test('seeded product appears in UI', async ({ request, page }) => { const res = await request.post('/api/products', { data: { name: 'Test Widget', price: 9 } }); const { id } = await res.json(); await page.goto('/products'); await expect(page.getByText('Test Widget')).toBeVisible(); await request.delete(`/api/products/${id}`); // cleanup});
Practical Example (UI create → API verify)
test('UI registration persists to backend', async ({ page, request }) => { const email = `qa+${Date.now()}@example.com`; await page.goto('/register'); await page.getByLabel('Email').fill(email); await page.getByLabel('Password').fill('P@ssw0rd!'); await page.getByRole('button', { name: 'Create account' }).click(); await expect(page).toHaveURL(/welcome/); // verify at the backend const res = await request.get(`/api/users?email=${encodeURIComponent(email)}`); expect(res.status()).toBe(200); expect((await res.json()).data.length).toBe(1);});
Line-by-Line Explanation
- UI performs the real registration a user would do.
- After the UI confirms success, an API GET verifies the user truly exists in the backend — catching cases where the UI “succeeds” but persistence fails.
Common Mistakes
- Duplicating auth logic instead of sharing a token/fixture across API and UI.
- Not cleaning up API-seeded data.
- Verifying only the UI (missing backend persistence bugs) or only the API (missing rendering bugs).
Best Practices
- Prefer API for setup/teardown; UI for the journey under test.
- Verify at both layers for critical flows.
- Share authentication (token/storageState) between API and UI.
- Make seeded data unique and clean it up.
Interview Questions
- Q: Why mix API and UI in one test? A: Fast/reliable setup + verification of real backend effects of UI actions.
- Q: When seed via API vs UI? A: Seed via API (fast, stable); exercise the feature under test via UI.
- Q: How do you confirm a UI action persisted? A: Query the API/DB after the UI reports success.
Practice Exercise
Write both patterns: (A) API-seed a product and assert it renders; (B) register via UI and verify via API. Add cleanup.
Real-World Scenario
An order test creates the customer and cart via API (0.5s), performs only checkout via UI, then verifies the order status via API. It’s fast, stable, and proves the UI checkout actually created a valid backend order.
FAQs
Can Playwright test APIs and UI in the same test?
Yes. The request fixture shares configuration (and optionally cookies) with the browser context, so one test can call APIs and drive the UI.
Why seed test data through the API?
It is far faster and less flaky than creating data through the UI, and keeps each UI test focused on the behaviour it actually checks.
Note: reqres.in now asks for a free API key sent in an x-api-key header; check its site, or use another practice API such as JSONPlaceholder or Restful Booker.