1. Why API tests?
- Faster — no browser launch, no rendering, no auto-wait overhead.
- More stable — no flaky selectors or animation timing.
- Cheaper — runs in CI in seconds, not minutes.
- Catches backend bugs before they ever reach the UI layer.
2. APIRequestContext basics
The built-in request fixture gives you an HTTP client that lives alongside your UI tests — same runner, same reporter, same trace viewer.
import { test, expect } from '@playwright/test';
test('GET /users returns 200', async ({ request }) => {
const response = await request.get('https://reqres.in/api/users?page=1');
expect(response).toBeOK();
const body = await response.json();
expect(body.data.length).toBeGreaterThan(0);
});3. Status code assertions
expect(response).toBeOK()— any 2xx response.expect(response.status()).toBe(201)— exact match for created resources.expect(response.status()).toBe(404)— assert negative cases too.
const created = await request.post('/api/users', { data: { name: 'morpheus' } });
expect(created.status()).toBe(201);
const missing = await request.get('/api/users/99999');
expect(missing.status()).toBe(404);4. Body validation
Use toMatchObject to assert a partial shape — ignore noisy fields like id and createdAt.
const body = await response.json();
expect(body).toMatchObject({ name: 'morpheus', job: 'leader' });5. Auth
Pass extraHTTPHeaders when you create a request context, or per call via the headers option.
// Bearer token
const api = await request.newContext({
baseURL: 'https://api.example.com',
extraHTTPHeaders: { Authorization: `Bearer ${process.env.TOKEN}` },
});
// Basic auth — one-off
await request.get('https://api.example.com/me', {
headers: { Authorization: 'Basic ' + btoa('user:pass') },
});
// OAuth2 client credentials — fetch a token first
const tokenRes = await request.post('https://auth.example.com/oauth/token', {
form: {
grant_type: 'client_credentials',
client_id: process.env.CLIENT_ID!,
client_secret: process.env.CLIENT_SECRET!,
},
});
const { access_token } = await tokenRes.json();6. Hands-on task
Hit the public reqres.in API and write one test each for GET, POST, and DELETE. Assert status, body shape, and headers.
test('list users', async ({ request }) => {
const r = await request.get('https://reqres.in/api/users?page=2');
expect(r).toBeOK();
expect((await r.json()).data.length).toBeGreaterThan(0);
});
test('create user', async ({ request }) => {
const r = await request.post('https://reqres.in/api/users', {
data: { name: 'morpheus', job: 'leader' },
});
expect(r.status()).toBe(201);
expect(await r.json()).toMatchObject({ name: 'morpheus' });
});
test('delete user', async ({ request }) => {
const r = await request.delete('https://reqres.in/api/users/2');
expect(r.status()).toBe(204);
});7. What's next
Next, move into Module 07: Data-driven testing — loop the same test over CSV/JSON fixtures to cover edge cases without copy-paste.
Up next in the learning path
Module 07: Data-driven testing