Guides

Playwright API Testing: Complete Guide (2026)

Smeet GohelUpdated Aug 19, 202612 min read

Quick answer

Playwright can test REST APIs directly through its built-in request fixture, with no browser instance required — a meaningful advantage over browser-based tools, since API-only suites run faster and need no browser binaries installed in CI. Pair it with the ajv library for JSON schema validation and Playwright's own test runner for parametrization and reporting. Teams already using Playwright for end-to-end tests get API testing in the same framework and CI job, with no second tool to maintain.

Reviewed by Sushant Joshi

Share:
Grid comparing whether Playwright, Cypress, and pytest need a browser and can drive UI end-to-end tests

Playwright API testing means using Playwright's built-in request fixture to send and assert on HTTP calls directly, without launching a browser — the same test runner, assertion library (expect()), and reporting that Playwright uses for end-to-end tests, applied to the API layer alone. Teams already using Playwright for UI automation get API coverage in the same framework with no second tool to learn.

This guide builds a real Playwright API test suite from scratch: project setup, the request fixture, JSON schema validation with ajv, parametrized tests, a custom fixture for authentication, and a GitHub Actions workflow. Every code sample runs as written against JSONPlaceholder, a free public fake REST API.

Table of Contents

  1. Why Playwright for API Testing
  2. What You Need
  3. Project Structure
  4. Writing Your First API Test
  5. Validating Response Schemas with ajv
  6. Testing POST, PUT, and DELETE Requests
  7. Parametrizing Tests Without a Decorator
  8. Custom Fixtures for Authentication
  9. Running Your Suite in CI/CD with GitHub Actions
  10. Common Pitfalls in Playwright API Testing
  11. Playwright vs Postman vs pytest vs AI-Generated Tests
  12. When to Move Beyond Hand-Written Tests
  13. FAQ

Why Playwright for API Testing

Playwright is best known as a browser automation framework, but its request fixture — an instance of APIRequestContext — sends HTTP calls with no browser attached at all. That matters for two reasons: an API-only suite starts and runs faster with no browser binary to launch, and teams that already use Playwright for end-to-end tests get API coverage in the exact same test runner, fixture model, and CI configuration, rather than adopting a second framework for the API layer.

The tradeoff against a Python- or Java-specific tool is ecosystem, not capability: Playwright's API testing surface (request.get(), request.post(), response.json()) is comparable to requests or REST Assured, but it lives in a JavaScript/TypeScript codebase — the natural fit for teams whose UI and API tests should share one language.

Playwright API testing stack showing the request fixture, playwright/test, ajv, and CI/CD stages

What You Need

npm install -D @playwright/test ajv
  • Node.js 18+
  • @playwright/test — the test runner and request fixture
  • ajv — JSON schema validation (Playwright has no built-in equivalent)

You do not need npx playwright install for a pure API suite — that command downloads browser binaries, which an API-only test never launches.

Project Structure

api-tests/
├── playwright.config.ts
├── package.json
└── tests/
    ├── users.spec.ts
    ├── posts.spec.ts
    └── schemas/
        └── user-schema.ts
// playwright.config.ts
import { defineConfig } from '@playwright/test';

export default defineConfig({
  testDir: './tests',
  use: {
    baseURL: 'https://jsonplaceholder.typicode.com',
  },
  reporter: [['list'], ['junit', { outputFile: 'results.xml' }]],
});

baseURL in the shared use block means every test can call request.get('/users/1') with a relative path instead of repeating the full URL.

Writing Your First API Test

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

test('get user returns 200', async ({ request }) => {
  const response = await request.get('/users/1');
  expect(response.status()).toBe(200);
});

test('get user returns expected fields', async ({ request }) => {
  const response = await request.get('/users/1');
  const body = await response.json();
  expect(body.id).toBe(1);
  expect(body.email).toContain('@');
  expect(body.name).toBeTruthy();
});

Run it:

npx playwright test

response.ok() is a convenience shortcut for status() >= 200 && status() < 300 when you only care whether the call succeeded, not the exact code — useful for setup steps inside a fixture where the specific status isn't the thing under test.

Validating Response Schemas with ajv

Field-by-field assertions do not catch a field silently changing type or an unexpected field appearing.

// tests/schemas/user-schema.ts
export const userSchema = {
  type: 'object',
  required: ['id', 'name', 'username', 'email'],
  properties: {
    id: { type: 'integer' },
    name: { type: 'string' },
    username: { type: 'string' },
    email: { type: 'string', format: 'email' },
    address: { type: 'object' },
    phone: { type: 'string' },
    website: { type: 'string' },
    company: { type: 'object' },
  },
};
import { test, expect } from '@playwright/test';
import Ajv from 'ajv';
import addFormats from 'ajv-formats';
import { userSchema } from './schemas/user-schema';

const ajv = new Ajv();
addFormats(ajv);

Ready to shift left with your API testing?

Try our no-code API test automation platform free. Generate tests from OpenAPI, run in CI/CD, and scale quality.

test('user matches schema', async ({ request }) => { const response = await request.get('/users/1'); const body = await response.json(); const validate = ajv.compile(userSchema); const valid = validate(body); expect(valid, JSON.stringify(validate.errors)).toBeTruthy(); });


`ajv-formats` is a separate package (`npm install -D ajv-formats`) required for format keywords like `"format": "email"` — plain `ajv` validates types and required fields but ignores `format` without it. Passing `JSON.stringify(validate.errors)` as the assertion message means a failure shows exactly which field violated the schema, not just that validation failed.

## Testing POST, PUT, and DELETE Requests

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

test('create post', async ({ request }) => {
  const response = await request.post('/posts', {
    data: { title: 'foo', body: 'bar', userId: 1 },
  });
  expect(response.status()).toBe(201);
  const body = await response.json();
  expect(body.title).toBe('foo');
  expect(body.id).toBeDefined();
});

test('update post', async ({ request }) => {
  const response = await request.put('/posts/1', {
    data: { id: 1, title: 'updated', body: 'bar', userId: 1 },
  });
  expect(response.status()).toBe(200);
  const body = await response.json();
  expect(body.title).toBe('updated');
});

test('delete post', async ({ request }) => {
  const response = await request.delete('/posts/1');
  expect(response.status()).toBe(200);
});

Passing data as a plain object serializes it to JSON automatically and sets Content-Type: application/json — the same convenience requests gives you in Python with json=payload.

Parametrizing Tests Without a Decorator

Playwright has no @parametrize decorator. Instead, loop over your cases at the module level and call test() inside the loop — each call registers as its own named test:

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

const cases = [
  { userId: 1, expectedStatus: 200 },
  { userId: 10, expectedStatus: 200 },
  { userId: 999, expectedStatus: 404 },
];

for (const { userId, expectedStatus } of cases) {
  test(`get user ${userId} returns ${expectedStatus}`, async ({ request }) => {
    const response = await request.get(`/users/${userId}`);
    expect(response.status()).toBe(expectedStatus);
  });
}

Because the loop runs at module load time, not at test-run time, Playwright's reporter lists all three as distinct, individually re-runnable tests — get user 999 returns 404 appears as its own line, not folded into a shared parametrized-test entry.

Custom Fixtures for Authentication

Extend Playwright's base test with a project-specific fixture instead of re-authenticating inside every test:

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

type AuthFixtures = {
  authToken: string;
};

export const test = base.extend<AuthFixtures>({
  authToken: async ({}, use) => {
    const token = process.env.TEST_API_TOKEN ?? '';
    await use(token);
  },
});

export { expect };
import { test, expect } from './fixtures';

test('get protected resource', async ({ request, authToken }) => {
  const response = await request.get('/account', {
    headers: { Authorization: `Bearer ${authToken}` },
  });
  expect(response.status()).toBe(200);
});

Reading the token from process.env.TEST_API_TOKEN keeps it out of the repository — set it as an encrypted secret in your CI provider, never as a literal string in a spec file.

Running Your Suite in CI/CD with GitHub Actions

# .github/workflows/api-tests.yml
name: API Tests

on: [push, pull_request]

jobs:
  api-tests:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: '20'
      - run: npm ci
      - run: npx playwright test
        env:
          TEST_API_TOKEN: ${{ secrets.TEST_API_TOKEN }}
      - uses: actions/upload-artifact@v4
        if: always()
        with:
          name: playwright-report
          path: playwright-report/

Notice there is no npx playwright install step — for a pure API suite there are no browsers to install, which is one of the reasons this pipeline runs faster than an equivalent end-to-end Playwright job. See our step-by-step CI/CD guide for GitLab CI and Jenkins equivalents.

Common Pitfalls in Playwright API Testing

  • Forgetting await on response.json(). It returns a Promise; a missing await silently produces a Promise object instead of the parsed body, and every field assertion after it fails confusingly.
  • Asserting only the status code. Validate structure with ajv, not just individual fields.
  • Installing browser binaries for an API-only suite. Skip npx playwright install entirely if no test uses page — it only slows CI down.
  • No negative-path coverage. Loop invalid IDs and malformed payloads into the same parametrized array as valid cases.
  • Sharing mutable state between parametrized test cases. Each test() call in the loop should be independent — do not rely on one iteration's side effects being visible to the next.
  • Committing tokens into fixture files. Read them from process.env, set as CI secrets.

Free Guided worksheet

Build Your Testing Strategy in 30 Minutes

A structured worksheet that walks you through defining your testing strategy in 30 minutes. Cover architecture, tools, layers, and team responsibilities.

Download Free

Playwright vs Postman vs pytest vs AI-Generated Tests

ApproachLanguageLearning curveCI/CD fitSchema validationBest for
Playwright (request fixture)TypeScript/JavaScriptLow–MediumNative — same runner as E2E testsManual (ajv)Teams already using Playwright for E2E
pytest + requestsPythonLowNative — same runner as unit testsManual (jsonschema)Python teams
REST AssuredJavaMediumNative — same runner as JUnit/TestNGAutomatic (json-schema-validator)Java/JVM teams
Postman + NewmanJSON collections, JS scriptsLow for exploration, higher for CIRequires exporting/running via Newman CLIManual (pm.test scripts)Manual exploration, small automated suites
AI-generated (Total Shift Left)None requiredLow — import a specNative CI pluginsAutomatic — generated from the OpenAPI schemaTeams testing many endpoints across services

Playwright's specific advantage over pytest and REST Assured isn't capability — it's consolidation: a team already running Playwright for UI tests adds API coverage with zero new tooling. See API testing vs UI testing for how the two layers complement each other, or our full tools comparison for the wider field.

When to Move Beyond Hand-Written Tests

The economics are the same regardless of language: twice the endpoints means roughly twice the spec files, fixtures, and schema definitions to maintain. For a service with a dozen endpoints, hand-written Playwright tests are a reasonable, fast-to-set-up choice. Somewhere in the range of a few dozen endpoints across multiple services, the suite's maintenance cost starts competing with the API's own development for engineering time.

The alternative is generating the suite directly from the OpenAPI specification instead of hand-translating it into TypeScript. Total Shift Left imports your spec, generates positive, negative, and boundary test cases for every endpoint, and re-generates automatically when the spec changes — so schema drift updates the suite instead of silently breaking it.

Frequently Asked Questions

Can Playwright test APIs without a browser? Yes. The request fixture sends HTTP requests directly, independent of Playwright's browser automation — no browser binary needs installing for a pure API suite.

Playwright vs Postman for API testing — which is better? Postman is faster for manual exploration. Playwright is better for automated, CI/CD-integrated regression testing that lives in your codebase and uses the same runner as your end-to-end suite.

How do I validate a JSON schema in a Playwright API test? Playwright has no built-in schema validator — pair it with ajv. Compile a JSON Schema and run it against the parsed response body, then assert the result is valid.

How do I run parametrized API tests in Playwright? Loop over an array of cases at the module level and call test() inside the loop — each iteration registers as its own named test.

Should teams already using Playwright for E2E tests also use it for API tests? Usually yes — it catches backend regressions faster than routing every check through the browser, and reuses the existing runner, CI configuration, and reporting.

Can a Playwright API suite fully replace an AI-generated API test suite? For a handful of endpoints, hand-written Playwright is often faster to set up. Past a few dozen endpoints across services, generating the suite from an OpenAPI spec keeps coverage complete without the manual upkeep.

Key Takeaways

  • Playwright's request fixture tests APIs with no browser required — skip npx playwright install entirely for a pure API suite.
  • ajv fills the schema-validation gap Playwright's core library doesn't cover; add ajv-formats for format keywords like email.
  • Parametrize with a loop, not a decorator — call test() inside a for...of at module level so each case is its own reportable test.
  • base.extend() centralizes auth instead of re-reading a token in every test.
  • Teams already on Playwright for E2E gain API coverage with zero new tooling — same runner, same CI job, same reports.
  • Hand-written suites scale linearly with endpoint count. Past a few dozen endpoints across services, generating tests from an OpenAPI spec becomes the more maintainable path.

Generate the Equivalent Suite from Your OpenAPI Spec

Every test in this guide can also be generated automatically. Import your OpenAPI specification into Total Shift Left and get positive, negative, and boundary test cases for every endpoint — regenerated the moment your spec changes, with no Playwright fixtures to maintain by hand.

Start your free trial and compare the generated suite against your own, or see plans and pricing if you're already evaluating.

Ready to shift left with your API testing?

Try our no-code API test automation platform free.