SoftwareTestPilot
Module 06 · Lesson 1intermediate 14 min read API

API Testing with Playwright's Request Context

Skip the browser. Playwright's request context lets you test REST APIs directly — with auth, schema validation, mocks and blazing-fast setup for UI tests.

Quick answer

Use the built-in request fixture: await request.post('/api/users', { data }). It runs outside any browser, supports the same auth as your UI, and returns a typed APIResponse you can assert on directly.

1. Your first API test

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

test('GET /api/users returns 200 with a list', async ({ request }) => {
  const res = await request.get('https://api.example.com/users');
  expect(res.status()).toBe(200);
  const body = await res.json();
  expect(Array.isArray(body)).toBe(true);
  expect(body[0]).toHaveProperty('id');
});

test('POST creates and DELETE removes', async ({ request }) => {
  const post = await request.post('/api/users', {
    data: { name: 'Ada', email: 'ada@acme.co' },
  });
  expect(post.status()).toBe(201);
  const { id } = await post.json();

  const del = await request.delete(`/api/users/${id}`);
  expect(del.status()).toBe(204);
});

2. Authentication that actually scales

playwright.config.ts
ts
export default defineConfig({
  use: {
    baseURL: 'https://api.example.com',
    extraHTTPHeaders: {
      Authorization: `Bearer ${process.env.API_TOKEN}`,
      Accept: 'application/json',
    },
  },
});
global-setup.ts
ts
import { request } from '@playwright/test';
export default async () => {
  const ctx = await request.newContext();
  const res = await ctx.post('/oauth/token', {
    form: { grant_type: 'client_credentials', client_id: '...', client_secret: '...' },
  });
  process.env.API_TOKEN = (await res.json()).access_token;
};

3. JSON schema / contract validation

import { z } from 'zod';

const User = z.object({
  id: z.string().uuid(),
  email: z.string().email(),
  createdAt: z.string().datetime(),
});

test('user contract holds', async ({ request }) => {
  const res = await request.get('/api/users/me');
  User.parse(await res.json());  // throws if the shape drifted
});
Fail fast on contract drift
Wire the same schemas into your generated TypeScript client so drift breaks the build, not a downstream UI test.

4. Mocking a backend from a UI test

test('shows empty state when API returns []', async ({ page }) => {
  await page.route('**/api/orders', route => route.fulfill({
    status: 200, contentType: 'application/json', body: '[]',
  }));
  await page.goto('/orders');
  await expect(page.getByText('No orders yet')).toBeVisible();
});

test('shows error banner on 500', async ({ page }) => {
  await page.route('**/api/orders', route => route.fulfill({ status: 500 }));
  await page.goto('/orders');
  await expect(page.getByRole('alert')).toContainText(/couldn't load/i);
});

5. Blend API setup with UI assertions

Create data via API (fast), assert via UI (real user perspective). This alone can cut a suite runtime in half.

test('user sees their new invoice', async ({ page, request }) => {
  // 1. seed via API
  const res = await request.post('/api/invoices', { data: { amount: 42 } });
  const { id } = await res.json();

  // 2. verify in UI
  await page.goto(`/invoices/${id}`);
  await expect(page.getByRole('heading', { name: /invoice #/i })).toBeVisible();
  await expect(page.getByTestId('total')).toHaveText('$42.00');
});

6. Common mistakes

Driving login through the UI in every test

Log in once via API, save the token / cookies, reuse for the rest of the run.

Asserting only status code

200 with the wrong body is worse than a 500. Always assert shape and key values.

Hard-coding IDs from prod data

Create the data your test needs — never depend on a magic ID that vanishes when someone cleans the DB.

7. Hands-on task (30 minutes)

  1. 1

    GET / POST / DELETE

    Against https://reqres.in, write 3 tests covering GET list, POST create, DELETE.

  2. 2

    Add zod schema

    Validate the GET list response with a z.array(User) schema.

  3. 3

    Mock a 500

    In a UI test, stub the same endpoint to return 500 and assert an error banner appears.

Now zoom out: Framework Architecture.

Frequently asked questions

1.Can Playwright test APIs without a browser?
Yes. The `request` fixture (aka APIRequestContext) issues HTTP calls with no browser overhead — 10–20× faster than the same call driven through the UI. Use it for setup, teardown and pure API contracts.
2.Playwright vs Postman vs REST Assured for API testing?
Playwright wins when you need to blend API + UI in the same suite, share auth state, and run in the same CI job. Postman is great for exploratory work; REST Assured for JVM shops.
3.How do I authenticate an API request?
Pass `extraHTTPHeaders` when creating the context, or use a `storageState` file mined from a UI login. For OAuth flows, fetch a token in a `globalSetup` and re-use it.
4.How do I validate the JSON response schema?
Combine with `ajv` or `zod`. Define the schema once, assert every response. Contract mismatches now fail fast instead of leaking into UI tests.
5.Can I mock APIs in Playwright?
Yes — `page.route(url, handler)` intercepts and replies with fixtures. Perfect for testing error states without touching the backend.
6.Should API tests share the request context across tests?
One context per worker is a good default: reuses connections and auth. Reset state between tests via API not UI when possible.

Related lessons