Anastasiia Sokolinska

Written by: Chief Operating Officer

Anastasiia Sokolinska

Posted: 01.10.2026

13 min read

Most teams don't decide to test APIs in Playwright. They inherit their way into it.

You have Postman collections nobody has opened since the engineer who wrote them left, a RestAssured suite the backend team runs on its own schedule, and a Playwright E2E suite that's the only thing anyone still trusts. Three pipelines, three reports, three sets of credentials.

Here's what consolidating into Playwright actually solves, what it doesn't, and the failure modes that only appear once you pass a few dozen tests.

Get senior SDETs who migrate API suites into Playwright without losing coverage

Why teams end up testing APIs in Playwright instead of a dedicated tool

Testing is the single most common thing developers do with APIs, reported by 81% of respondents in Postman's 2025 State of the API Report. The same survey found 93% of API teams hitting collaboration blockers, with duplicate effort near the top of the list. Duplicate effort in API testing usually has a specific shape: two suites asserting the same endpoints in two languages, owned by two people who never talk.

Playwright API testing doesn't win on assertion syntax. It wins because the API tests, the UI tests, the auth setup, the environment config, and the report all live in one repository and run through one pipeline.

Architecture diagram showing API and UI projects feeding into a shared CI pipeline with unified build, test, and deployment stages, generating a single consolidated test report.

The consolidation pressure is real enough that Postman shipped its own Playwright integration in 2026, running collection assertions alongside Playwright specs. When the incumbent starts building bridges to your framework, the direction of travel is settled.

What APIRequestContext actually gives you

Every test receives a request fixture: an isolated APIRequestContext that honors baseURL, extraHTTPHeaders, and proxy settings from your config. It sends GET, POST, PUT, PATCH, and DELETE without launching a browser.

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

test('creates an order in pending state', async ({ request }) => {
  const response = await request.post('/api/orders', {
    data: { sku: 'SKU-1042', quantity: 2 },
  });
  expect(response.status()).toBe(201);
  const order = await response.json();
  expect(order.status).toBe('pending');
});

The distinction worth internalizing: page.request and context.request share a cookie jar with the browser context, so a request made through them carries the session your UI test is already using. request.newContext() gives you a standalone client with its own cookie storage, which is what you want for pure API specs. The official API testing docs cover the surface area; the interesting problems start one layer up.

Authenticating API tests without re-running login flows

Run the arithmetic on your own suite before deciding this doesn't matter. 300 tests, each doing a 4-second UI login, spends 20 minutes per run doing nothing but authenticating.

The standard fix is storageState: log in once in a setup project, write the cookies and origin storage to disk, then load that state into both browser contexts and API request contexts. That works until your CI run outlives your token.

Access tokens with 15-minute lifetimes are ordinary. A sharded suite that runs 40 minutes will hand a dead token to its last third of tests, and the failures look like intermittent 401s rather than an expiry problem, which is why teams chase them for weeks. Issue one token per worker, keyed on testInfo.parallelIndex, and refresh it before it expires:

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

type Token = { value: string; expiresAt: number };
const tokens = new Map<number, Token>();

async function issueToken(workerIndex: number): Promise<Token> {
  const context = await request.newContext({ baseURL: process.env.API_URL });
  const response = await context.post('/auth/token', {
    data: {
      username: `ci-worker-${workerIndex}@example.test`,
      password: process.env.CI_USER_PASSWORD!,
    },
  });
  const { access_token, expires_in } = await response.json();
  await context.dispose();
  return { value: access_token, expiresAt: Date.now() + expires_in * 1000 };
}

export const test = base.extend<{ apiClient: APIRequestContext }>({
  apiClient: async ({}, use, testInfo) => {
    const index = testInfo.parallelIndex;
    let token = tokens.get(index);
    // Refresh 60s early so a long shard never sends an expired token.
    if (!token || token.expiresAt - Date.now() < 60_000) {
      token = await issueToken(index);
      tokens.set(index, token);
    }
    const context = await request.newContext({
      baseURL: process.env.API_URL,
      extraHTTPHeaders: { Authorization: `Bearer ${token.value}` },
    });
    await use(context);
    await context.dispose();
  },
});

export { expect } from '@playwright/test';

Per-worker credentials also stop the other failure mode: two workers logging in as the same user, and one invalidating the other's session on a backend that enforces single-session-per-account.

Consolidate your test stack with DeviQA's Playwright engineers

Schema validation is not contract testing

These get conflated constantly, and the conflation causes real damage because a team running API testing with Playwright that ships schema assertions believes it has contract coverage. It doesn't.

Zod gives you structural validation inside the test, with the strict mode catching fields the provider added without telling you:

import { z } from 'zod';
import { test, expect } from '../fixtures/api';

const Order = z.object({
  id: z.string().uuid(),
  status: z.enum(['pending', 'paid', 'cancelled']),
  total: z.number().nonnegative(),
  createdAt: z.string().datetime(),
}).strict();

test('order response matches the published schema', async ({ apiClient }) => {
  const response = await apiClient.get('/api/orders/latest');
  expect(response.status()).toBe(200);
  Order.parse(await response.json());
});

Where the line falls:

Schema validation (Zod, JSON Schema)
Contract testing (Pact)

What it checks

The shape and types of one response from one service

The agreement between a consumer's expectations and a provider's actual behavior

Where it runs

Inside your Playwright test, against a deployed environment

Consumer and provider pipelines separately, coordinated through a broker

Catches

Renamed fields, wrong types, missing fields, unannounced additions

A provider shipping a change that breaks a consumer you neither own nor deploy

Misses

Breakage for consumers your tests never call

Runtime behavior of the assembled stack

Who needs it

Every team with an API

Teams with many services on independent release cycles

Only 17% of teams in the Postman survey run contract testing at all, against 67% for functional and integration testing. That gap is not a reason to relabel your schema assertions. It's a reason to know which one you're missing.

Combining API and UI tests in one suite, and where it breaks down

Seeding through the API before a UI test is the highest-return pattern in the framework. An API call resolves in tens of milliseconds; the browser equivalent, navigating a signup form and waiting for hydration, takes seconds. A worker-scoped fixture pays the setup cost once per worker instead of once per test:

export const test = base.extend<{}, { tenant: Tenant }>({
  tenant: [async ({}, use, workerInfo) => {
    const context = await request.newContext({ baseURL: process.env.API_URL });
    const shard = process.env.SHARD_INDEX ?? '0';
    const response = await context.post('/api/test-tenants', {
      data: { name: `shard-${shard}-worker-${workerInfo.workerIndex}` },
    });
    const tenant = await response.json();
    await use(tenant);
    await context.delete(`/api/test-tenants/${tenant.id}`);
    await context.dispose();
  }, { scope: 'worker' }],
});

Now the honest part. This pattern couples your UI tests to your API test state, and that coupling is invisible in the code. A UI test fails, an engineer reads the spec file, and nothing in it explains that a fixture three directories away created the record.

The sharper problem is shared infrastructure. Four shards pointed at one database will collide on unique constraints, fixed IDs, and cleanup routines that delete records another shard is mid-assertion on. This is especially common in API testing with Playwright, where parallel workers spin up fast and hit the same backend before anyone's thought about isolation. Namespace by shard as shown above, or give each shard an ephemeral schema or container. Skip this and you get failures that reproduce only in CI, only sometimes, which is the same signature as flaky test root causes and gets misdiagnosed as one.

Migrating a test stack into Playwright is a project we've run many times over, and our Playwright testing services exist for exactly this.

Negative testing: timeouts, retry budgets, and latency assertions

"Test your error cases" is advice nobody can act on. Three patterns you can:

Latency budgets as assertions. Treat response time as a functional requirement, not a performance-testing concern that lives in another tool and another quarter.

test('search stays inside its latency budget', async ({ apiClient }) => {
  const started = Date.now();
  const response = await apiClient.get('/api/search?q=widget');
  const elapsed = Date.now() - started;
  expect(response.status()).toBe(200);
  expect(elapsed).toBeLessThan(400);
});

Retry exhaustion. Assert that the client gives up rather than hanging, using a per-request timeout well under the test timeout.

test('gives up on a hung report endpoint', async ({ apiClient }) => {
  await expect(
    apiClient.get('/api/reports/heavy', { timeout: 2_000 })
  ).rejects.toThrow(/Timeout/);
});

Malformed input. Send the wrong type, not the missing field. Most validation layers catch absence and let a string through where an integer belongs.

GraphQL: why a 200 response can still be a failure

GraphQL sits at 33% adoption in the Postman data, and it trips up every team new to testing it for the same reason: business-logic failures return HTTP 200 with an errors array. A suite that asserts on status codes will report green through an outage.

test('rejects a withdrawal above the available balance', async ({ apiClient }) => {
  const response = await apiClient.post('/graphql', {
    data: {
      query: `mutation Withdraw($amount: Int!) {
        withdraw(amount: $amount) { balance }
      }`,
      variables: { amount: 999_999 },
    },
  });
  expect(response.status()).toBe(200);
  const body = await response.json();
  expect(body.data.withdraw).toBeNull();
  expect(body.errors?.[0]?.extensions?.code).toBe('INSUFFICIENT_FUNDS');
});

Assert on errors and on the extension code, never on the message string. Messages get reworded; codes are part of the contract.

Running API tests in CI: separate projects, workers, and shards

Split API and UI into separate projects. A 600-test API suite averaging 120 ms per test runs in roughly 72 seconds serially. Behind browser-suite worker limits, it waits.

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

export default defineConfig({
  fullyParallel: true,
  forbidOnly: !!process.env.CI,
  reporter: process.env.CI ? 'blob' : 'html',
  projects: [
    { name: 'api', testDir: './tests/api', use: { baseURL: process.env.API_URL } },
    {
      name: 'ui',
      testDir: './tests/ui',
      dependencies: ['api'],
      use: { baseURL: process.env.APP_URL, trace: 'on-first-retry' },
    },
  ],
});

Worker count is a global setting, not a per-project one, so the split has to happen at the job level:

jobs:
  api-tests:
    runs-on: [self-hosted, linux]
    steps:
      - uses: actions/checkout@v4
      - run: npm ci
      - run: npx playwright test --project=api --workers=8

Workers and shards solve different problems:

Workers
Shards

What it controls

Parallel processes on one machine

Splitting the test list across machines

Ceiling

Available cores on that runner

Runners you're willing to pay for

How you set it

workers in config, or --workers

--shard=1/4

Reporting

One report

One blob per shard, combined with merge-reports

Reach for it when

The suite is slow on a machine with spare cores

Serial time on a saturated machine is still too long

For that 600-test API suite, 8 workers on one self-hosted runner brings it under 15 seconds. Sharding adds checkout, install, and a merge job to every run, so it costs more than it saves at this size. Sharding earns its overhead on the browser suite, where per-test cost is seconds rather than milliseconds. If you're sizing this for a suite already past the hour mark, the tradeoffs are covered in parallel execution and sharding.

If your pipeline currently runs three separate test jobs producing three separate reports, that's the first thing worth fixing, and it doesn't require rewriting a single assertion.

Where Playwright stops making sense

Playwright's API layer is strongest when one team tests one API and one UI against one environment. Move away from that shape and the case weakens fast.

If you have a dozen services with independent release cycles and consumers you don't deploy, schema assertions in an E2E suite will not protect you. A provider can ship a breaking change on Tuesday, and you find out when your suite runs against the shared environment on Thursday. Pact exists for exactly this: consumer expectations published to a broker, verified in the provider's own pipeline before it merges. That is a different job, and Playwright does not do it.

Two other honest limits. Playwright has no GUI for exploratory poking at an endpoint, so a Postman workspace often survives consolidation as an exploration and documentation surface even after the automated collections are gone. And if your backend is Java and the tests belong to backend engineers reviewing them in the same PR as the service code, RestAssured stays where it is. Moving those tests into TypeScript buys a shared pipeline at the cost of the people who maintain them.

What consolidation actually buys you

Be specific about the return, because it's smaller than most migration pitches suggest and more durable than they admit.

You get one CI pipeline instead of two or three, which matters most at 2am when someone needs to work out which suite failed. You get one reporting surface: Playwright's merge-reports tool, shipped in v1.37, folds every shard into a single HTML report with traces attached. You get one language, which means the engineer who fixes a flaky UI test can also fix a broken schema assertion.

What you don't get is fewer tests to maintain. A Postman collection with pre-request scripts and OAuth helpers becomes real code that someone has to write, review, and own. Budget the migration as engineering work, not as an export. Teams that treat it as a conversion script end up with 400 generated specs nobody understands, which is a worse position than the one they started in. The same failure pattern shows up in tool migrations generally, including migrating from AccelQ.

Common mistakes that surface after the first 50 tests

  • Sharing one service account across all workers. Works at 5 tests, produces phantom 401s at 50.

  • Asserting only on status codes. A 200 with a null payload passes. So does a GraphQL error.

  • Reusing storageState without checking expiry. The failures cluster at the end of long runs and look random.

  • Cleaning up in afterAll instead of the fixture teardown. A crashed worker leaves orphaned records that break the next run.

  • Running API and UI tests under the same worker count. The fast suite inherits the slow suite's ceiling.

  • Calling schema validation "contract testing" in a status report. Leadership stops asking about the gap that's actually there.

Conclusion

Playwright will not make your API tests better than RestAssured's. It makes them cheaper to own, because one team maintains one pipeline in one language against one environment.

That's the whole argument. If your API and UI are tested by different people on different release cycles, the consolidation math doesn't work, and you should keep the tools you have.

Inherited a fragmented API/UI test stack? Get a scoped audit of your Playwright migration path. We'll map what converts cleanly, what needs rewriting, and what belongs in a dedicated contract-testing tool before anyone touches the codebase.

Book a strategic QA consultation

Anastasiia Sokolinska

About the author

Anastasiia Sokolinska

Chief Operating Officer

Anastasiia Sokolinska is the Chief Operating Officer at DeviQA, responsible for operational strategy, delivery performance, and scaling QA services for complex software products.