PRACTICAL GUIDE / playwright api testing

Playwright API Testing: Validate APIs Inside Your E2E Suite

Playwright API testing guide covering request contexts, setup flows, assertions, authentication, fixtures, UI strategy, and CI-ready checks.

By The Testing AcademyUpdated July 10, 202610 min read
All field guides
In this guide9 sections
  1. Separate three testing jobs
  2. Configure request defaults and secrets
  3. Write an API test around business state
  4. Cover validation and authorization deliberately
  5. Check protocol details that clients rely on
  6. Validate shape without freezing incidental fields
  7. Build a typed setup fixture
  8. Combine API setup with one UI assertion
  9. Share authentication only when the model permits it
  10. Diagnose responses without leaking data
  11. Make API checks safe under parallel CI

What you will learn

  • Separate three testing jobs
  • Configure request defaults and secrets
  • Write an API test around business state
  • Cover validation and authorization deliberately

A browser test that creates a customer through six UI screens before checking a refund has two unrelated reasons to fail. If onboarding breaks, the refund test never reaches its subject. If the setup account already exists, the result depends on yesterday's run. Playwright API testing is most valuable when it removes that accidental coupling while keeping the browser assertion focused on user behavior.

This guide builds a TypeScript project for an order service. API tests verify the order contract, an API fixture creates isolated data, and one UI test confirms that a refund appears in the customer portal. The boundaries are intentional: direct requests prepare and inspect state, while the browser covers what a customer sees.

Separate three testing jobs

Organize the suite according to purpose:

  • API regression specs exercise HTTP status, authorization, validation, and persisted state without a browser.
  • Setup fixtures create the minimum records required by a UI test.
  • Browser specs verify presentation and interaction, then use the API only when backend confirmation adds value.

Do not route every service check through the browser. Conversely, do not call a UI journey "end to end" if every backend response is intercepted with a stub. The repository should make each boundary obvious from directory names and test titles.

Example
tests/
├── api/
│   └── orders.spec.ts
├── e2e/
│   └── refund-status.spec.ts
└── fixtures/
    └── order-fixture.ts

Configure request defaults and secrets

Playwright Test's built-in request fixture uses configuration from the selected project. Set a base URL and non-secret headers centrally. Read credentials from environment variables and fail early when CI forgot to provide them.

TypeScript
// playwright.config.ts
import { defineConfig } from '@playwright/test'

const apiToken = process.env.TEST_API_TOKEN
if (!apiToken) throw new Error('TEST_API_TOKEN is required')

export default defineConfig({
  testDir: './tests',
  use: {
    baseURL: process.env.TEST_BASE_URL ?? 'http://127.0.0.1:3000',
    extraHTTPHeaders: {
      authorization: `Bearer ${apiToken}`,
      'x-test-client': 'playwright'
    },
    trace: 'retain-on-failure'
  },
  reporter: [['list'], ['html', { open: 'never' }]]
})

Never check a personal bearer token into the project. Prefer a limited test principal whose permissions match the test role. If the product uses several identities, create named fixtures instead of changing global headers during a test.

Write an API test around business state

An order creation check should assert more than status 201, but less than the entire JSON document. Assert the contract fields that downstream behavior depends on, then read the resource back to prove persistence.

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

test('creates a pending order with the calculated total', async ({ request }) => {
  const externalId = `pw-${test.info().parallelIndex}-${Date.now()}`

  const create = await request.post('/api/orders', {
    data: {
      externalId,
      currency: 'USD',
      items: [{ sku: 'NOTE-A5', quantity: 2 }]
    }
  })

  expect(create.status()).toBe(201)
  expect(create.headers()['content-type']).toContain('application/json')

  const order = await create.json()
  expect(order).toMatchObject({
    externalId,
    status: 'pending',
    currency: 'USD',
    itemCount: 2
  })
  expect(order.id).toEqual(expect.any(String))

  const read = await request.get(`/api/orders/${order.id}`)
  expect(read.status()).toBe(200)
  const persisted = await read.json()
  expect(persisted).toMatchObject({
    id: order.id,
    externalId,
    total: 2400
  })
})

The response check stays focused on fields that establish identity and price. Confirm whether money is represented in minor units in the real contract rather than copying 2400 blindly.

Cover validation and authorization deliberately

Negative tests should prove the service rejects a meaningful risk and returns a stable error shape. Do not generate random malformed bodies that nobody can diagnose.

TypeScript
test('rejects an order with no items', async ({ request }) => {
  const response = await request.post('/api/orders', {
    data: { externalId: 'empty-order', currency: 'USD', items: [] }
  })

  expect(response.status()).toBe(422)
  expect(await response.json()).toMatchObject({
    code: 'ORDER_ITEMS_REQUIRED',
    field: 'items'
  })
})

For authorization, create a separate request context with the intended identity. An isolated context prevents accidental reuse of the default privileged header.

TypeScript
import { request as playwrightRequest } from '@playwright/test'

test('prevents a customer from reading another account order', async () => {
  const customerClient = await playwrightRequest.newContext({
    baseURL: process.env.TEST_BASE_URL,
    extraHTTPHeaders: {
      authorization: `Bearer ${process.env.CUSTOMER_B_TOKEN}`
    }
  })

  try {
    const response = await customerClient.get('/api/orders/order-for-customer-a')
    expect(response.status()).toBe(404)
  } finally {
    await customerClient.dispose()
  }
})

Some products intentionally return 404 instead of 403 to avoid disclosing resource existence. Assert the documented security behavior, not a generic preference.

Check protocol details that clients rely on

Status and JSON fields are only part of an HTTP contract. Assert a Location header after creation if clients use it, cache headers on responses that must not be stored, and a correlation header needed for support. For conditional updates, test the documented ETag or version behavior with one valid update and one stale request.

Keep these checks selective. Copying every response header into a snapshot makes harmless infrastructure changes noisy. Each assertion should correspond to a client dependency, security rule, or production incident.

Test content negotiation when the API promises it. Send an unsupported content type and assert the documented rejection. Send an explicit accepted representation and confirm the response content type before parsing. This catches gateways that return an HTML error page with a successful-looking route.

Validate shape without freezing incidental fields

TypeScript types do not validate runtime JSON. A response can compile as any and still omit a required property. Use a schema library already approved by the repository or write focused runtime guards for important payloads. Validate required types and allowed enum values, then make business assertions separately.

A schema should tolerate fields the contract marks extensible. Exact deep equality against the complete body turns additive server changes into failures. Conversely, toMatchObject alone can miss a required field that vanished if the test never names it. Maintain the schema from the published API contract when one exists, and review changes alongside consumers.

Build a typed setup fixture

A fixture can give each browser test a unique order and guarantee cleanup. Keep the returned object small so the test does not depend on every API field.

TypeScript
// tests/fixtures/order-fixture.ts
import { test as base, expect } from '@playwright/test'

type Order = { id: string; externalId: string }
type Fixtures = { order: Order }

export const test = base.extend<Fixtures>({
  order: async ({ request }, use, testInfo) => {
    const externalId = `refund-${testInfo.workerIndex}-${Date.now()}`
    const response = await request.post('/api/orders', {
      data: {
        externalId,
        currency: 'USD',
        items: [{ sku: 'NOTE-A5', quantity: 1 }]
      }
    })
    expect(response.status()).toBe(201)
    const body = await response.json()

    await use({ id: body.id, externalId })

    const cleanup = await request.delete(`/test-support/orders/${body.id}`)
    expect(cleanup.status()).toBe(204)
  }
})

export { expect } from '@playwright/test'

Teardown runs after use, even when the test fails. Cleanup assertions can mask the original failure, so teams may prefer to attach cleanup details and fail separately. If deletion is unsafe, expire test records by namespace through a scheduled process.

Worker-scoped fixtures are useful for expensive immutable setup, such as a catalog shared by tests that only read it. They are dangerous for mutable orders. Decide scope from state semantics, not runtime alone. A test-scoped order gives every case a clean lifecycle and makes a failing ID easy to trace.

If setup creates several related records, return an aggregate containing only their IDs and define teardown in reverse dependency order. Log cleanup failures with those IDs. Do not expose the raw privileged setup client to every test because it encourages business actions that bypass the UI boundary without review.

Combine API setup with one UI assertion

The browser test imports the custom fixture, requests a refund by API, and verifies the portal's user-facing result. It does not retest the refund endpoint's entire schema.

TypeScript
// tests/e2e/refund-status.spec.ts
import { test, expect } from '../fixtures/order-fixture'

test('shows a requested refund in order history', async ({ page, request, order }) => {
  const refund = await request.post(`/api/orders/${order.id}/refunds`, {
    data: { reason: 'customer_request' }
  })
  expect(refund.status()).toBe(202)

  await page.goto(`/account/orders/${order.id}`)
  await expect(page.getByRole('heading', { name: `Order ${order.externalId}` }))
    .toBeVisible()
  await expect(page.getByTestId('refund-status'))
    .toHaveText('Refund requested')
})

A 202 response may mean processing is asynchronous. Page auto-waiting cannot make backend work finish. The application should expose a visible pending state, or the fixture should poll a status endpoint with a deadline before expecting a completed state. Never add an arbitrary timeout to guess processing duration.

Share authentication only when the model permits it

An API request context associated with a browser context can share cookie state. This is useful when login through an API creates the same session cookie the portal consumes. Token-only APIs may instead use explicit headers and a separate browser storage state.

Keep authentication setup in a dedicated project or fixture, store generated state in a test-artifact directory, and never commit it. Validate expiry behavior. A storage-state file produced yesterday can make local tests pass while CI starts unauthenticated.

Do not use an administrator context for customer flows simply because setup is easier. Setup may need elevated access, but the browser should operate as the role under test. Separate clients make that distinction reviewable.

Diagnose responses without leaking data

When an assertion fails, capture method, path, status, request correlation ID, and a redacted response excerpt. Playwright traces are excellent for browser activity, but direct API calls need purposeful attachments if their bodies matter.

TypeScript
const body = await response.text()
await test.info().attach('create-order-response', {
  body: body.replaceAll(/"email":"[^"]+"/g, '"email":"[redacted]"'),
  contentType: 'application/json'
})
expect(response.status(), body).toBe(201)

Only parse JSON after checking content type when an intermediary might return HTML. Preserve correlation IDs so service owners can find server logs. Do not attach tokens, card details, or full personal records. Artifact retention is part of the security design.

Differentiate connection failure, deadline expiry, HTTP error, invalid JSON, schema failure, and business assertion in test names and messages. "Expected true to be false" is not sufficient for an API gate. Include method, sanitized route, expected status, actual status, and correlation ID, but never place a bearer token or full query string containing personal data in the message.

For eventually consistent reads, collect the last observed state and polling duration. A timeout that reports only 30 seconds hides whether the resource never appeared, stayed pending, or moved to a terminal failure. That distinction determines whether the owning team is API, worker infrastructure, or test setup.

Make API checks safe under parallel CI

Run API-only specs separately from browser projects so failures and runtime are visible. Use worker-safe identifiers and ensure cleanup cannot delete another worker's data. If the environment enforces rate limits, set a deliberate worker count instead of letting parallelism overload it.

YAML
- name: API contract checks
  run: npx playwright test tests/api --workers=4
  env:
    TEST_BASE_URL: ${{ vars.TEST_BASE_URL }}
    TEST_API_TOKEN: ${{ secrets.TEST_API_TOKEN }}
    CUSTOMER_B_TOKEN: ${{ secrets.CUSTOMER_B_TOKEN }}

- name: Upload Playwright report
  if: always()
  uses: actions/upload-artifact@v4
  with:
    name: playwright-report
    path: playwright-report

Retry only operations that are safe to repeat. A failed POST may have created the order even if the response was lost. Prefer idempotency keys or a unique external ID that the service treats predictably. Blind request retries can create duplicate business records.

Split fast deterministic contract checks from destructive or integration-heavy cases. A pull request can run authorization, validation, and core order transitions against an isolated environment. Scheduled coverage can exercise provider sandboxes or long-running workflows with lower concurrency. Use test annotations or projects so this routing is visible in configuration rather than encoded in filename folklore.

Watch rate-limit headers and service saturation when increasing workers. A test account with privileged limits can hide the behavior ordinary clients receive, while an unrealistically small test limit can create false failures. Configure known test quotas and assert rate limiting in dedicated cases, not accidentally across the whole suite.

Keep contract assertions close to the API specs, setup helpers small, and browser assertions centered on customer-visible behavior. That structure gives each failure one likely owner and lets the suite use HTTP speed without pretending the API and UI are the same testing layer.

Review that boundary whenever a new service or user role enters the workflow.

// FIELD DISPATCH

Get the QA Field Notes

Weekly QA battles, AI testing guides, and interview drills. Free on Substack.

// LIVE COURSE / THE TESTING ACADEMY

Playwright Automation Mastery

Go beyond Selenium. Master Playwright with JS/TS in 90 days.

From the instructor behind this guide.

Playwright jobs are growing 8x faster than Selenium. 90 days / 75+ live hrs / Tue-Thu-Sat 7 AM IST.

Code PROMODE / 10% offJoin the batch

The Testing Academy editorial desk

Practical QA guidance built around test evidence, production tradeoffs, and interview-ready explanations.

Published July 10, 2026 / Reviewed July 10, 2026

PRIMARY REFERENCES

Verify the details at the source

QABattle guides are practical explanations. Product behavior, standards, and APIs can change, so use these primary references for the canonical details.

  1. 01
    Playwright documentation

    Microsoft

    Canonical API, locator, fixture, browser, and test-runner behavior.

  2. 02
    Playwright best practices

    Microsoft

    Official guidance for resilient tests, isolation, and user-facing locators.

  3. 03
    HTTP Semantics

    IETF

    The normative semantics for methods, status codes, fields, and HTTP behavior.

  4. 04
    OWASP API Security Top 10

    OWASP Foundation

    Primary API-specific risk taxonomy and defensive guidance.

FAQ / QUICK ANSWERS

Questions testers ask

Can Playwright be used for API testing?

Yes. Playwright includes request APIs that can send HTTP requests, manage headers, store authentication state, and assert responses. It is especially useful when API checks support browser tests, although dedicated API tools may still be better for broad contract, performance, or exploratory API work.

Should API tests be in the same project as UI tests?

They can be, if the APIs support the same product flows and the team benefits from shared fixtures. Keep pure API regression tests organized separately from UI E2E tests. Mixing every check together can make failures harder to understand and pipelines slower.

Is Playwright API testing better than Postman?

They solve different problems. Playwright is excellent when API calls prepare or verify E2E browser flows and when engineers want tests in code. Postman is excellent for interactive exploration, collections, documentation, and collaboration. Mature teams often use both for different layers.

How do I handle authentication in Playwright API tests?

Use request contexts with headers, login endpoints, storage state, or fixtures that create authenticated clients. Avoid hard coding personal tokens. Use test users, environment variables, and setup routines that make authentication repeatable in local and CI environments.

What should Playwright API tests assert?

Assert status code, response schema, important fields, error messages, authorization behavior, and state changes that matter to the product. Do not assert every field blindly. Focus on contract expectations and business outcomes that would break users or downstream systems.