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.
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:
- Keep one
openapi.yamlas the source of truth. - Share fixture data between Playwright and Apidog.
- Add lightweight API assertions inside Playwright user flows.
- Run deeper Apidog API scenarios in CI.
- 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);
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();
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
}
But the frontend expects:
{
"total_count": 42
}
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
}
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:
-
401expired token -
403insufficient permissions -
409idempotency conflict -
429rate limit -
500partial 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
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 };
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);
});
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:
- Open your project.
- Click Import.
- Select the same
openapi.yaml. - Generate endpoints, request examples, and schemas.
- Save shared payloads as environment variables or data sets.
- 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);
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
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
Add scripts:
{
"scripts": {
"test:e2e": "playwright test",
"test:e2e:headed": "playwright test --headed"
}
}
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,
});
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}
Export scenarios for CLI execution, for example:
apidog-cli run ./apidog/scenarios/checkout.json
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);
});
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 }}
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'
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
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
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 }) => {
// ...
});
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' });
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.routemocks 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
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:
- Pick one critical journey, such as checkout or signup.
- Add the Playwright API fixture.
- Import the same OpenAPI spec into Apidog.
- Build one matching Apidog scenario.
- Run both in CI.
- 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);
};
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)