DEV Community

Cover image for How to Validate API responses inside Playwright tests ?
Hassann
Hassann

Posted on • Originally published at apidog.com

How to Validate API responses inside Playwright tests ?

Your Playwright tests pass: the login button clicks, the dashboard renders, and the chart appears. Then a customer reports that the chart numbers are wrong. The API returned 200 OK with a malformed payload, but your end-to-end suite only checked that pixels appeared on screen. Browser tests alone will not catch that class of failure. You need API assertions that validate contracts, schemas, and response semantics. Tools like Apidog help you test API behavior with the same rigor as UI flows, then run both suites in CI.

Try Apidog today

TL;DR

Use Playwright’s request fixture and page.route interceptor for API-aware browser tests, then run Apidog scenarios against the same OpenAPI spec for schema validation, chained workflows, mocks, and error paths.

The practical setup:

  1. Keep one openapi.yaml as the source of truth.
  2. Share fixture data between Playwright and Apidog.
  3. Add lightweight API assertions inside Playwright user flows.
  4. Run deeper Apidog API scenarios in CI.
  5. Fail the build when either UI behavior or API contracts break.

Introduction

Playwright makes API testing look simple in the official documentation:

const response = await request.get('/api/orders');
expect(response.status()).toBe(200);
Enter fullscreen mode Exit fullscreen mode

That works for a smoke check. It is not enough for a production API surface with dozens or hundreds of endpoints.

Common problems appear quickly:

  • Tests check status codes but not response shape.
  • Browser flows and API tests use different fixture data.
  • UI tests pass even when API business logic is wrong.
  • Error paths like expired tokens, rate limits, and partial failures are not covered.
  • Frontend teams cannot easily mock APIs when staging is unstable.

The fix is to treat your OpenAPI spec as the contract. Playwright should use it indirectly through shared fixtures and boundary-level assertions. Apidog should import the same spec and run deeper API scenarios for schemas, workflows, and negative cases.

If you want to install the tool first, Download Apidog, then follow the setup below.

For broader context on tool selection, see API testing tools for QA engineers.

The gap between Playwright tests and API assertions

A typical Playwright test verifies a user-visible flow:

await page.goto('/dashboard');
await expect(page.getByTestId('revenue-chart')).toBeVisible();
Enter fullscreen mode Exit fullscreen mode

That confirms the chart rendered. It does not prove that the API returned correct data.

Three failure modes often slip through.

1. Payload shape regressions

An endpoint returns:

{
  "totalCount": 42
}
Enter fullscreen mode Exit fullscreen mode

But the frontend expects:

{
  "total_count": 42
}
Enter fullscreen mode Exit fullscreen mode

The UI may render 0, N/A, or fallback content. If your Playwright test only checks that the chart exists, it passes.

2. Business logic drift

A discount endpoint returns a 10% rebate instead of the contracted 15%:

{
  "discount_pct": 10
}
Enter fullscreen mode Exit fullscreen mode

The UI displays whatever the API returns. The browser test passes unless it explicitly checks the expected business value.

3. Error path gaps

Browser suites usually focus on the happy path. APIs also need coverage for:

  • 401 expired token
  • 403 insufficient permissions
  • 409 idempotency conflict
  • 429 rate limit
  • 500 partial downstream failure
  • malformed request bodies
  • missing required fields

You can add request.get() calls inside Playwright specs, but Playwright is still primarily a browser automation framework. It is not optimized for large stateful API workflow suites like:

create order → fetch order → cancel order → verify refund → validate webhook retry

A better split is:

  • Playwright: UI flows, network interception, and high-value API assertions around user actions.
  • Apidog: schema validation, chained API workflows, contract compliance, mocks, and error-path coverage.

Both should consume the same OpenAPI contract. For more on this model, read contract-first development tooling.

How to share fixtures between Playwright and Apidog

Use one source of truth:

repo/
├─ openapi.yaml
├─ fixtures/
│  └─ order.json
├─ tests/
│  ├─ fixtures/
│  │  └─ api.ts
│  └─ orders.spec.ts
└─ apidog/
   └─ scenarios/
      └─ checkout.json
Enter fullscreen mode Exit fullscreen mode

Your openapi.yaml defines the API contract. Your fixtures/ folder stores reusable payloads. Playwright imports those fixtures directly. Apidog uses the same payloads as examples, environment variables, or data sets.

Create a Playwright API fixture

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

type ApiFixtures = {
  apiRequest: APIRequestContext;
  authToken: string;
  sampleOrder: Record<string, unknown>;
};

export const test = base.extend<ApiFixtures>({
  apiRequest: async ({ playwright }, use) => {
    const ctx = await playwright.request.newContext({
      baseURL: process.env.API_BASE_URL ?? 'https://api.staging.example.com',
      extraHTTPHeaders: {
        Accept: 'application/json',
        'Content-Type': 'application/json',
      },
    });

    await use(ctx);
    await ctx.dispose();
  },

  authToken: async ({ apiRequest }, use) => {
    const res = await apiRequest.post('/auth/token', {
      data: {
        email: 'qa@example.com',
        password: process.env.QA_PASSWORD,
      },
    });

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

    const body = await res.json();
    await use(body.access_token);
  },

  sampleOrder: async ({}, use) => {
    const raw = readFileSync(
      path.join(__dirname, '..', '..', 'fixtures', 'order.json'),
      'utf8',
    );

    await use(JSON.parse(raw));
  },
});

export { expect };
Enter fullscreen mode Exit fullscreen mode

Now import test from this fixture file instead of @playwright/test.

// tests/orders.spec.ts
import { test, expect } from './fixtures/api';

test('POST /orders returns a valid order with 15 percent discount', async ({
  apiRequest,
  authToken,
  sampleOrder,
}) => {
  const res = await apiRequest.post('/orders', {
    headers: {
      Authorization: `Bearer ${authToken}`,
    },
    data: {
      ...sampleOrder,
      coupon: 'SAVE15',
    },
  });

  expect(res.status()).toBe(201);

  const body = await res.json();

  expect(body).toMatchObject({
    id: expect.any(String),
    status: 'pending',
    discount_pct: 15,
    total_cents: expect.any(Number),
  });

  expect(body.total_cents).toBeLessThan(sampleOrder.subtotal_cents);
});
Enter fullscreen mode Exit fullscreen mode

This Playwright test checks:

  • the endpoint returns 201
  • the order has an ID
  • the status is correct
  • the discount logic is correct
  • the total is less than the subtotal

That is a useful boundary-level API assertion.

Import the same contract into Apidog

In Apidog:

  1. Open your project.
  2. Click Import.
  3. Select the same openapi.yaml.
  4. Generate endpoints, request examples, and schemas.
  5. Save shared payloads as environment variables or data sets.
  6. Build scenarios for critical workflows.

For the same POST /orders flow, Apidog can validate the full response against the Order schema in openapi.yaml.

Playwright catches the high-value semantic assertion:

expect(body.discount_pct).toBe(15);
Enter fullscreen mode Exit fullscreen mode

Apidog catches schema drift across every field:

  • missing required fields
  • incorrect types
  • invalid enum values
  • renamed properties
  • malformed nested objects

For more on spec-driven workflows, see design-first API workflows.

If your team is moving from Postman, self-hosted Postman alternatives covers migration considerations.

Set up the Apidog + Playwright workflow

Here is a repeatable implementation path.

Step 1: Commit one OpenAPI spec

Put the spec at the repo root:

openapi.yaml
Enter fullscreen mode Exit fullscreen mode

Treat it like application code:

  • review changes in PRs
  • version breaking changes
  • keep examples updated
  • use it to generate mocks and tests
  • prevent unreviewed contract drift

If you do not have a spec yet, generate a draft from your framework if possible. FastAPI, NestJS, and many other frameworks can emit OpenAPI definitions.

Apidog can also help create a spec from imported traffic, such as a HAR file.

Step 2: Wire Playwright

Install Playwright:

npm init playwright@latest
Enter fullscreen mode Exit fullscreen mode

Add scripts:

{
  "scripts": {
    "test:e2e": "playwright test",
    "test:e2e:headed": "playwright test --headed"
  }
}
Enter fullscreen mode Exit fullscreen mode

Use environment variables for API targets:

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

export default defineConfig({
  use: {
    baseURL: process.env.WEB_BASE_URL ?? 'http://localhost:3000',
    trace: 'on-first-retry',
  },
  retries: 2,
});
Enter fullscreen mode Exit fullscreen mode

Keep Playwright specs focused. One user journey per test is easier to debug than one giant end-to-end flow.

Step 3: Add Apidog scenario coverage

In Apidog, create scenarios for critical API workflows:

  • signup
  • login
  • checkout
  • refund
  • subscription upgrade
  • webhook delivery
  • permission changes
  • password reset
  • rate limit behavior

Each scenario should chain requests and assertions.

Example workflow:

POST /orders
GET /orders/{id}
POST /orders/{id}/cancel
GET /refunds/{refund_id}
Enter fullscreen mode Exit fullscreen mode

Export scenarios for CLI execution, for example:

apidog-cli run ./apidog/scenarios/checkout.json
Enter fullscreen mode Exit fullscreen mode

Step 4: Use Playwright network interception for UI isolation

When you need deterministic UI tests, intercept network calls with page.route.

test('dashboard renders cached order list when offline', async ({
  page,
  sampleOrder,
}) => {
  await page.route('**/api/orders', async (route) => {
    await route.fulfill({
      status: 200,
      contentType: 'application/json',
      body: JSON.stringify({
        orders: [sampleOrder],
      }),
    });
  });

  await page.goto('/dashboard');

  await expect(page.getByTestId('order-row')).toHaveCount(1);
});
Enter fullscreen mode Exit fullscreen mode

Use this for isolation, not as a replacement for API tests.

The same sampleOrder fixture should also be used in Apidog scenarios, so your mock data stays aligned with the API contract.

Step 5: Run both suites in CI

Use separate jobs so failures are easy to identify.

name: tests

on: [push, pull_request]

jobs:
  playwright:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-node@v4
        with:
          node-version: '20'

      - run: npm ci

      - run: npx playwright install --with-deps

      - run: npx playwright test
        env:
          WEB_BASE_URL: ${{ secrets.WEB_BASE_URL }}
          API_BASE_URL: ${{ secrets.API_BASE_URL }}
          QA_PASSWORD: ${{ secrets.QA_PASSWORD }}

  apidog:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-node@v4
        with:
          node-version: '20'

      - run: npm i -g apidog-cli

      - run: apidog-cli run ./apidog/scenarios/checkout.json --reporters cli,junit
        env:
          API_BASE_URL: ${{ secrets.API_BASE_URL }}
          QA_PASSWORD: ${{ secrets.QA_PASSWORD }}
Enter fullscreen mode Exit fullscreen mode

Failing either job should block the merge.

Use JUnit output so your CI system can annotate failures in PRs. The GitHub Actions documentation explains matrix builds, caching, and artifacts if you need to scale this further.

For ownership models, see API testing tool for QA engineers.

Step 6: Add drift detection

Schedule a daily job that compares the current API contract with the version used by your tests.

At minimum, fail the build when:

  • a response field changes type
  • a required field is removed
  • an enum value changes unexpectedly
  • an endpoint disappears
  • an error response shape changes

This catches the original problem: 200 OK with the wrong payload.

Advanced techniques and pro tips

Enable Playwright traces

Set this in playwright.config.ts:

trace: 'on-first-retry'
Enter fullscreen mode Exit fullscreen mode

When a test flakes in CI, the trace gives you:

  • DOM snapshots
  • network calls
  • console logs
  • screenshots
  • timing information

Pair it with Apidog HTML or JUnit reports to determine whether the UI failed first or the API contract drifted.

Use Apidog mock servers for offline work

Apidog can create mock responses from your OpenAPI spec. This is useful when:

  • staging is down
  • the backend team is mid-deploy
  • the database is being reset
  • frontend work is blocked by an unfinished endpoint

Run Playwright against the mock server for deterministic UI feedback, then run Apidog scenarios against the real backend when staging is healthy.

For a related workflow, see AI-assisted API test generation.

Keep retries low

Use:

retries: 2
Enter fullscreen mode Exit fullscreen mode

If a test needs five retries, it is not stable. Fix the root cause.

For API scenarios, keep request-level retries conservative as well. Retrying too aggressively can hide real reliability issues.

Fail closed on schema drift

Schema mismatch should fail CI by default.

If your team needs a temporary exception, make it explicit:

ALLOW_SCHEMA_DRIFT=true apidog-cli run ./apidog/scenarios/checkout.json
Enter fullscreen mode Exit fullscreen mode

Require a PR comment explaining why the exception is safe and when it will be removed.

Tag tests by priority

Use tags to control CI cost:

test('checkout happy path @smoke', async ({ page }) => {
  // ...
});

test('refund with expired token @regression', async ({ apiRequest }) => {
  // ...
});
Enter fullscreen mode Exit fullscreen mode

Suggested split:

  • @smoke: every push
  • @regression: PRs to main
  • @nightly: full browser + API scenario suite

For stateful Playwright flows, configure serial execution when needed:

test.describe.configure({ mode: 'serial' });
Enter fullscreen mode Exit fullscreen mode

Avoid these mistakes

  • Only asserting status === 200
  • Hardcoding bearer tokens
  • Maintaining separate fixture files for Playwright and Apidog
  • Skipping the API CLI in CI
  • Treating page.route mocks as API coverage
  • Letting the OpenAPI spec drift from production behavior
  • Testing only happy paths

For AI-agent APIs and nondeterministic outputs, see how to test AI agents API.

Alternatives and tooling comparison

Several stacks can validate APIs alongside browser tests.

Stack Strengths Weaknesses Best for
Playwright alone (request fixture) One tool, fast, native to the suite Shallow schema validation, no chained scenarios, weak error-path coverage Small teams, simple APIs
Playwright + Postman Mature Postman ecosystem, Newman CLI Collections can drift from OpenAPI; collaboration may require paid plans Teams already deep in Postman
Playwright + Apidog Single OpenAPI source, schema validation, mocks, CLI for CI, design-first workflow Two tools to learn; requires spec discipline Teams that want spec-driven API and UI coverage
Cypress + cy-api plugin Familiar to Cypress teams API testing is constrained by the Cypress model; plugin maturity varies Existing Cypress codebases
Pact Strong consumer-driven contract guarantees Steeper learning curve, broker infrastructure, not focused on UI flows Microservice teams with many internal API consumers

If you are migrating from older SOAP-era tools, read SoapUI Groovy script alternatives and ReadyAPI alternatives.

For local-first workflows, see REST client VSCode extensions.

Real-world use cases

E-commerce checkout

Use Playwright for the cart-to-confirmation browser flow.

Use Apidog for the API chain:

create payment intent
run fraud check
reserve inventory
create order
confirm payment
send receipt
Enter fullscreen mode Exit fullscreen mode

If a payment gateway response changes from error_code to errorCode, Apidog catches the schema drift directly. Playwright may only show a generic checkout failure.

SaaS dashboard chart data

Use Playwright to verify that dashboards render.

Use Apidog to validate aggregation endpoints:

  • sums
  • averages
  • p95/p99 latency
  • grouped time buckets
  • empty result sets
  • filter combinations

A chart can look correct while the underlying aggregation is wrong. API assertions catch the data error before users do.

Webhook-driven workflow

Use Playwright for the user-facing portal.

Use Apidog scenarios for:

  • webhook delivery
  • signature validation
  • retry logic
  • duplicate event rejection
  • idempotency
  • eventual consistency checks

This is difficult to cover from a browser-only suite.

Conclusion

Playwright is excellent for browser flows. It is not enough for deep API validation.

A practical Playwright + Apidog setup gives you:

  • one OpenAPI contract
  • shared fixture data
  • schema-level validation
  • browser-level user flow coverage
  • API workflow coverage
  • mock servers for offline development
  • CI failures for both UI and API regressions
  • clear ownership between frontend and backend tests

Start small:

  1. Pick one critical journey, such as checkout or signup.
  2. Add the Playwright API fixture.
  3. Import the same OpenAPI spec into Apidog.
  4. Build one matching Apidog scenario.
  5. Run both in CI.
  6. Expand endpoint and error-path coverage over time.

FAQ

Can I validate APIs in Playwright tests without Apidog?

Yes. Use Playwright’s request fixture and manual expect calls.

That works for status checks and selected body assertions. For schema validation, chained scenarios, mocks, and error-path coverage at scale, a dedicated API testing tool like Apidog is more practical.

See API testing tool for QA engineers for trade-offs.

Do I need an OpenAPI spec?

You need one to get the full benefit.

Without a spec, you can still run Playwright and Apidog side by side, but you lose the shared contract and may duplicate payload examples across tools.

How should I handle authentication?

Fetch a fresh token at runtime.

In Playwright, expose it through a fixture:

authToken: async ({ apiRequest }, use) => {
  const res = await apiRequest.post('/auth/token', {
    data: {
      email: 'qa@example.com',
      password: process.env.QA_PASSWORD,
    },
  });

  const body = await res.json();
  await use(body.access_token);
};
Enter fullscreen mode Exit fullscreen mode

In Apidog, store the token in an environment variable and reuse it across scenario steps.

Can Apidog replace Playwright?

No.

Apidog validates API behavior. It does not render the browser. You still need Playwright for:

  • clicks
  • forms
  • visible text
  • layout behavior
  • client-side routing
  • browser console errors
  • user-facing flows

Use both tools for different surfaces.

What if staging is unstable?

Use Apidog’s mock server from your OpenAPI spec.

Point Playwright at the mock API for deterministic frontend testing. Run Apidog scenarios against the real backend when staging is available.

How do I keep CI fast?

Use priority tags:

  • smoke tests on every push
  • regression tests on PRs to main
  • full API scenarios nightly

Parallelize Playwright with workers and run API scenarios through the CLI where appropriate.

Do I need a paid Apidog plan for CI?

Check the current Apidog pricing page before adopting at scale. For small teams, the free tier may cover many workflows, but plan details can change.

Top comments (0)