API AUTOMATIONINTERMEDIATE

API Testing with Playwright: Request Context, Auth, and Assertions

Use Playwright's request context to test REST APIs — send requests, verify status and JSON, share auth state with UI tests.

iff Solution Academy July 4, 2026 15 min read Updated July 7, 2026
Playwright API REST Request Context

Introduction

Most teams learn Playwright for UI automation and never realise it also ships with a first-class HTTP client. Playwright's request context — accessed as request in every test — is a fully featured Node.js HTTP client that supports cookies, storage state, base URLs, custom headers, HTTPS options, and shared authentication with UI tests. That means you can write REST and GraphQL tests without pulling in axios, supertest, or a separate framework.

This tutorial walks through building a complete API test suite with Playwright: from your first GET request, through a full CRUD flow, to sharing an authenticated session across both API and UI tests. The examples are runnable against the public https://reqres.in and https://jsonplaceholder.typicode.com sandboxes, so you can follow along without any backend of your own.

Why API Testing with Playwright?

If you already own a Playwright test suite for UI, using the same runner for APIs is a substantial productivity win:

  • One test runner, one config, one HTML report covering both API and UI.
  • One CI pipeline — no separate Postman/Newman or REST-assured job.
  • Shared TypeScript types between UI and API tests.
  • Hybrid tests: seed data via API in one line, verify it in the UI in the next.
  • Built-in retries, sharding, parallelisation, and trace viewer — same features you already use for UI.
  • No extra dependencies — request is part of @playwright/test.

You also inherit Playwright's excellent debugging story: failing API requests appear in the HTML report with full request/response bodies, timing information, and headers.

Prerequisites

  • Playwright installed and running — see our Getting Started guide if not.
  • Node.js 18+ and TypeScript basics.
  • Familiarity with REST concepts: HTTP methods, status codes, JSON bodies, headers.
  • A REST API to test — this tutorial uses https://reqres.in as a free sandbox.

Step 1: The Request Context

The simplest API test uses the built-in request fixture. Each test gets an isolated context, so cookies and auth do not leak between tests:

tests/api/users.spec.ts
import { test, expect } from '@playwright/test';

test('list users returns 200 and a data array', async ({ request }) => {
  const res = await request.get('https://reqres.in/api/users?page=2');

  expect(res.status()).toBe(200);
  expect(res.ok()).toBeTruthy();

  const body = await res.json();
  expect(Array.isArray(body.data)).toBe(true);
  expect(body.data.length).toBeGreaterThan(0);
});

To share a base URL and default headers across a whole suite, configure them once in playwright.config.ts:

playwright.config.ts
use: {
  baseURL: 'https://reqres.in',
  extraHTTPHeaders: {
    Accept: 'application/json',
    'x-api-key': process.env.API_KEY ?? '',
  },
},

Now every request({ ... }) call can use relative paths.

Step 2: A Full CRUD Example

A realistic API suite covers the full lifecycle of a resource. This spec creates a user, updates them, reads them back, and deletes them — asserting the correct status code at each step:

tests/api/user-crud.spec.ts
import { test, expect } from '@playwright/test';

test('user CRUD lifecycle', async ({ request }) => {
  // CREATE
  const created = await request.post('/api/users', {
    data: { name: 'Ada Lovelace', job: 'Engineer' },
  });
  expect(created.status()).toBe(201);
  const user = await created.json();
  expect(user).toMatchObject({ name: 'Ada Lovelace', job: 'Engineer' });
  const id = user.id;

  // UPDATE
  const updated = await request.put(`/api/users/${id}`, {
    data: { name: 'Ada L.', job: 'Author of first algorithm' },
  });
  expect(updated.ok()).toBeTruthy();

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

Notice how Playwright's expect().toMatchObject() lets you assert on partial JSON — you check what matters and ignore server-generated fields like createdAt.

Step 3: Sharing Auth State with UI Tests

UI logins are slow and flaky. Real teams log in once via API, save the cookies and localStorage, and reuse them for every UI test. Playwright makes this trivial with storageState.

global-setup.ts
import { request } from '@playwright/test';

export default async () => {
  const api = await request.newContext();
  await api.post('https://reqres.in/api/login', {
    data: { email: 'eve.holt@reqres.in', password: 'cityslicka' },
  });
  // Persist cookies + localStorage for every UI project to reuse.
  await api.storageState({ path: 'state.json' });
  await api.dispose();
};
playwright.config.ts
export default defineConfig({
  globalSetup: './global-setup.ts',
  use: { storageState: 'state.json' },
});

Every subsequent test starts with the user already logged in — reducing suite time by 30–70% on login-heavy apps.

Step 4: Response Assertions

Beyond status codes, Playwright's expect() can assert on headers, JSON shape, and even schemas via matchers:

ts
// Status
expect(res.status()).toBe(201);
expect(res.ok()).toBeTruthy();

// Headers
expect(res.headers()['content-type']).toContain('application/json');

// JSON shape
const body = await res.json();
expect(body).toMatchObject({ id: expect.any(Number), name: 'Ada' });
expect(body.tags).toEqual(expect.arrayContaining(['engineer']));

// Negative assertion
expect(body).not.toHaveProperty('password');
For strict schema validation, combine Playwright with a library like zod or ajv to assert against a full JSON schema.

Step 5: Hybrid API + UI Flows

The killer use case: seed test data via API (fast, deterministic) then verify it in the UI (real user behaviour).

ts
test('newly created order appears in the dashboard', async ({ page, request }) => {
  // 1. Seed via API — fast
  const res = await request.post('/api/orders', { data: { total: 42 } });
  const order = await res.json();

  // 2. Verify via UI — real user path
  await page.goto('/dashboard');
  await expect(page.getByRole('row', { name: `Order ${order.id}` })).toBeVisible();
});

Step 6: Reusable API Fixtures

Once you have more than a handful of tests, extract an authenticated API client as a custom fixture:

fixtures/api.ts
import { test as base, expect, APIRequestContext } from '@playwright/test';

type ApiFixtures = { api: APIRequestContext };

export const test = base.extend<ApiFixtures>({
  api: async ({ playwright }, use) => {
    const api = await playwright.request.newContext({
      baseURL: 'https://reqres.in',
      extraHTTPHeaders: { Authorization: `Bearer ${process.env.TOKEN}` },
    });
    await use(api);
    await api.dispose();
  },
});
export { expect };

Every test that imports { test } from this file receives a preconfigured, authenticated API client — no boilerplate per test.

Common Pitfalls

  • Forgetting to await request.* — every HTTP call is async.
  • Calling res.json() twice — the body stream can only be consumed once. Store it in a variable.
  • Hard-coding cookies or tokens in the test file — always read from process.env or storageState.
  • Testing against a shared staging environment without cleanup — flaky, order-dependent tests. Prefer isolated test data per run.
  • Not calling api.dispose() on manually created contexts — resource leaks in long suites.
  • Assuming reqres.in / jsonplaceholder actually persist data — they simulate responses but do not save.
  • Mixing baseURL with absolute URLs by accident — either use one or the other consistently.

Debugging Tips

  • Log the raw body when an assertion fails: console.log(await res.text()) — always call text() (not json()) in a catch so malformed responses do not throw again.
  • Enable trace: 'on' in playwright.config.ts — the Trace Viewer shows every API request with headers, body, and timing.
  • Use --reporter=list temporarily for line-by-line output during local debugging.
  • Wrap a failing endpoint in a request.get(..., { timeout: 30_000 }) to rule out slow-network flakes.
  • Check res.headers() when authentication mysteriously fails — 401s often come with a WWW-Authenticate header explaining why.
  • Curl the same endpoint outside of Playwright to confirm the failure is not in your test code.

When to Use Playwright for API Testing (and When Not To)

Great fit

  • Teams that already own Playwright for UI and want to consolidate tooling.
  • Hybrid API + UI test flows (seed via API, verify via UI).
  • End-to-end regression suites where API and UI are covered together.
  • Auth flows that need cookies or storage state shared with UI tests.

Not the best choice

  • Contract testing — use Pact or Spring Cloud Contract.
  • Heavy load or performance testing — use k6, JMeter, or Gatling.
  • Deep protocol testing (gRPC streaming, WebSocket load) — use specialised tools.
  • Teams with no UI tests at all — a lighter tool like supertest may be simpler.

FAQ

Does the request context share cookies with page?

Only if you pass storageState between them, or use the same BrowserContext. By default request.newContext() is isolated from any page.

Can Playwright test GraphQL?

Yes — send a POST to the GraphQL endpoint with { query, variables } in the body. All the assertion tools work the same way.

How do I upload a file?

Pass a multipart object: request.post('/upload', { multipart: { file: fs.createReadStream('cv.pdf') } }).

How do I set a per-test header?

Pass headers directly on the call: request.get('/users', { headers: { 'x-trace': 'abc' } }). This overrides extraHTTPHeaders from the config.

Should I write API tests in the same file as UI tests?

For hybrid flows (seed + verify) yes. For pure API regression, keep them in a separate tests/api folder with their own project in playwright.config.ts.

Summary

Playwright's request context turns your UI automation framework into a complete API testing tool. You get status-code checks, JSON assertions, header validation, file uploads, cookie management, and storage-state sharing — all with the same familiar expect() API and the same Trace Viewer for debugging.

For most teams the biggest wins come from two patterns: (1) log in once via API and reuse storageState for every UI test, and (2) seed test data through the API before UI assertions. Together they cut suite time significantly while making tests dramatically more deterministic.

Get Playwright tutorials in your inbox

Weekly tips, real-world examples, and framework patterns – no spam, unsubscribe anytime.

Recommended Next Articles