SoftwareTestPilot
Module 06 · Lab 1
Intermediate
12 min read

API Testing with Playwright — APIRequestContext from A to Z

Test REST APIs without launching a browser. Drive GET, POST, auth, status codes, and response validation with Playwright's APIRequestContext.

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