Playwright·Cookbook Field Manual
Part 04 · Architecture, Auth & Data Card 23

API-Only Tests

Card 23: API-Only Tests

What This Pattern Solves

Not every test needs a browser. When you want to validate an HTTP API contract — status codes, response bodies, headers — launching a full browser with page rendering is wasteful. It’s slower, consumes more CI resources, and introduces visual rendering flakiness where none should exist. Playwright’s request fixture lets you write focused, fast API tests using the same test runner, assertions, and reporting you already use for browser tests.

How It Works

  1. Use the request fixture (an APIRequestContext) directly in tests — no page or browser needed
  2. Call request.get(url, { headers }) or request.post(url, { data }) to make HTTP calls
  3. Assert on response.status() for HTTP status codes (200, 404, etc.)
  4. Parse the body with await response.json() and assert on structured data
  5. Relative URLs work because request inherits baseURL from playwright.config.ts
  6. Tests run in parallel, fully isolated, and produce the same HTML report with passes, failures, and attachments

Code Example

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

interface User {
  id: string;
  name: string;
  role: string;
}

test.describe('23-api-only-tests: API tests with request fixture', () => {
  test('GET /api/health returns 200 and ok: true', async ({ request }) => {
    const response = await request.get('/api/health');
    expect(response.status()).toBe(200);

    const body = (await response.json()) as { ok: boolean };
    expect(body.ok).toBe(true);
  });

  test('GET /api/user/1 with Accept header returns user JSON', async ({
    request,
  }) => {
    const response = await request.get('/api/user/1', {
      headers: { Accept: 'application/json' },
    });
    expect(response.status()).toBe(200);

    const body: User = await response.json();
    expect(body).toEqual({ id: '1', name: 'Alice', role: 'admin' });
    expect(body.id).toBe('1');
    expect(body.name).toBe('Alice');
    expect(body.role).toBe('admin');
  });

  test('GET /api/user/999 returns 404', async ({ request }) => {
    const response = await request.get('/api/user/999', {
      headers: { Accept: 'application/json' },
    });
    expect(response.status()).toBe(404);
    const body = (await response.json()) as { error: string };
    expect(body.error).toBe('Not found');
  });
});

Run This Example

pnpm test src/23-api-only-tests

Prerequisites

Key Concepts

When to Use This Pattern

Common Mistakes

  1. Forgetting await when calling response.json():

    // ❌ WRONG — body is a Promise, not the parsed object
    const body = response.json();
    expect(body.ok).toBe(true); // Always passes (Promise is truthy)
    
    // ✓ CORRECT — await the parsing
    const body = await response.json();
    expect(body.ok).toBe(true);
  2. Hardcoding full URLs instead of using baseURL:

    // ❌ WRONG — breaks when port or host changes
    await request.get('http://localhost:9321/api/health');
    
    // ✓ CORRECT — relative path, uses baseURL from config
    await request.get('/api/health');
  3. Not setting Accept header for content negotiation:

    // ❌ WRONG — server might return HTML instead of JSON
    const res = await request.get('/api/user/1');
    const body = await res.json(); // Might fail on HTML response
    
    // ✓ CORRECT — explicitly request JSON
    const res = await request.get('/api/user/1', {
      headers: { Accept: 'application/json' },
    });
  4. Mixing API and browser concerns in one test:

    // ❌ WRONG — using page to assert API responses
    test('health check', async ({ page }) => {
      const res = await page.request.get('/api/health');
    });
    
    // ✓ CORRECT — use the request fixture, no browser launched
    test('health check', async ({ request }) => {
      const res = await request.get('/api/health');
    });

Flow Diagram

sequenceDiagram
    participant Test
    participant Request as request fixture
    participant API as API Server
    participant Report as HTML Report

    Test->>Request: request.get('/api/health')
    Request->>API: GET http://localhost:9321/api/health
    API-->>Request: 200 OK  { ok: true }
    Request-->>Test: Response

    Test->>Test: expect(status).toBe(200) ✓
    Test->>Test: body = await response.json()
    Test->>Test: expect(body).toHaveProperty('ok', true) ✓

    Note over Report: Test passed ✓ (45ms)

    Test->>Request: request.get('/api/user/999')
    Request->>API: GET /api/user/999  (Accept: application/json)
    API-->>Request: 404 Not Found  { error: 'Not found' }
    Request-->>Test: Response

    Test->>Test: expect(status).toBe(404) ✓
    Test->>Test: expect(body).toHaveProperty('error', 'Not found') ✓

    Note over Report: Test passed ✓ (32ms)

Live Demo

👇 This component is what the Playwright test interacts with:

Loading…

Run This Example

pnpm test src/23-api-only-tests