API Contract Testing with Playwright: How I Combine REST Validation and UI Flows in One Test
Most QA teams still treat API testing and UI testing as separate kingdoms. The API team owns the contract suite in Postman or REST Assured. The automation team owns the browser suite in Playwright or Selenium. They run on different schedules, report to different channels, and rarely share state. The result is slow feedback, duplicated setup code, and bugs that slip through the gap between a green API test and a broken user interface.
I stopped accepting this separation two years ago. At Tekion, we now run API contract testing with Playwright as a single hybrid suite. One test creates a user via REST, validates the response against an OpenAPI schema, logs in through the browser using shared cookies, and confirms the dashboard renders the correct data. The test runs in 4 seconds. A pure UI version of the same flow takes 18 seconds. The hybrid version is also more reliable because it validates the contract before the browser ever opens.
In this guide, I will show you the exact pattern, the code, and the CI setup I use to combine REST validation and UI flows in one Playwright test.
Table of Contents
- What Is API Contract Testing and Why It Matters
- Why Playwright Is the Right Tool for Hybrid API + UI Testing
- The Hybrid Test Pattern: API Setup + Browser Validation
- OpenAPI Schema Validation Inside Playwright
- Cookie and Auth State Sharing Between API and Browser
- A Real Example: E-Commerce Checkout Flow
- CI/CD Integration: Docker and GitHub Actions
- Common Traps When Mixing API and UI Tests
- India Context: What Hiring Managers Pay for Hybrid Skills
- Key Takeaways
- FAQ
Contents
What Is API Contract Testing and Why It Matters
A contract test verifies that an API response matches a predefined schema. It does not test business logic. It tests structure. If the /user endpoint promises to return a field called email as a string, the contract test fails the moment that field becomes a number or disappears entirely.
This matters because frontend code depends on that structure. A missing email field does not just break an API test. It breaks the React component that renders the profile page. It breaks the TypeScript interface that the component imports. It breaks the form validation that expects a string. By the time a human spots the regression, three downstream services may have already ingested the malformed payload.
The Cost of Breaking Contracts in Production
Postman’s 2025 State of the API Report found that 62% of organizations experienced an API-breaking change in production at least once per quarter. The average time to detect and rollback was 3.4 hours. For a payment gateway processing ₹2 crores per hour, that is a ₹6.8 crore mistake. Contract testing catches these breaks before the code merges, not after customers complain.
Contract Testing vs Integration Testing
Many teams confuse the two. Integration testing asks: “Does the checkout flow work end to end?” Contract testing asks: “Does the /checkout response still contain orderId, total, and status with the correct types?” The questions are complementary. You need both. But contract tests are faster, more deterministic, and should run at every pull request.
- Integration tests: 200-2000ms per API call, validate logic and state.
- Contract tests: 20-80ms per response, validate schema and types.
- UI tests: 3000-8000ms per flow, validate user experience.
Why Playwright Is the Right Tool for Hybrid API + UI Testing
Playwright is not just a browser automation framework. It ships with a full-featured APIRequestContext that can send HTTP requests, handle cookies, manage headers, and reuse authentication state with the browser context. This is not an afterthought. Microsoft designed it specifically for the hybrid pattern I am describing.
The Download Numbers Do Not Lie
Playwright’s dominance in the automation market is not speculative. It is measurable:
@playwright/test: 141 million monthly npm downloads.playwrightcore: 212 million monthly npm downloads.- GitHub stars: 88,636 as of May 2026.
- Latest release: v1.60.0, published May 11, 2026.
Compare that to selenium-webdriver at 9.05 million monthly downloads. Playwright has 23x the npm volume. When a framework reaches this scale, third-party tooling, Stack Overflow answers, and hiring pipelines all align around it. Learning Playwright in 2026 is not a bet. It is the default.
Built-In API Testing vs External Tools
You could run API contract tests with Postman, REST Assured, or Python requests. But then you lose two critical advantages:
- Shared state: Playwright’s
APIRequestContextandBrowserContextcan share cookie storage. Log in via API, save the session, open a browser with that session already active. No login forms. No 2FA prompts. No time wasted. - Single report: One HTML trace, one Allure dashboard, one CI pipeline. When an API setup step fails, the trace viewer shows the exact request and response. When the UI step fails, it shows the screenshot and DOM snapshot. Everything is in one place.
The Hybrid Test Pattern: API Setup + Browser Validation
Here is the simplest version of the pattern. I use it in roughly 40% of our UI tests at Tekion.
Step 1: Create Data via API
Instead of clicking through three forms to create a test user, send one POST request:
import { test, expect } from '@playwright/test';
test('user dashboard shows correct profile', async ({ page, request }) => {
// Create user via API
const createRes = await request.post('/api/users', {
data: { name: 'Amit Sharma', email: 'amit@test.com', role: 'admin' }
});
expect(createRes.ok()).toBeTruthy();
const user = await createRes.json();
expect(user.id).toBeDefined();
Step 2: Validate the Contract
Before the browser opens, assert that the response matches your expected shape. I will show you the OpenAPI version in the next section. For now, a manual check:
// Contract validation
expect(user).toMatchObject({
id: expect.any(String),
name: 'Amit Sharma',
email: expect.stringContaining('@'),
role: 'admin',
createdAt: expect.any(String)
});
Step 3: Validate via UI
Now open the browser and confirm the user sees what the API promised:
// UI validation
await page.goto(`/dashboard/users/${user.id}`);
await expect(page.getByRole('heading', { name: 'Amit Sharma' })).toBeVisible();
await expect(page.getByText('amit@test.com')).toBeVisible();
await expect(page.getByText('admin')).toBeVisible();
});
This test runs in 2.8 seconds on my machine. The pure UI equivalent—creating the user through the registration form—takes 14 seconds and fails 8% of the time due to email validation race conditions. The hybrid pattern is faster and more stable.
OpenAPI Schema Validation Inside Playwright
Manual toMatchObject checks are fine for small payloads. They do not scale. When your API has 47 endpoints and the frontend team adds a new required field every sprint, you need machine-readable contracts. OpenAPI is that contract.
Why OpenAPI Dominates Contract Testing
The OpenAPI Specification repository has 30,920 GitHub stars and is maintained by the Linux Foundation. It is the de facto standard for describing REST APIs. Most backend frameworks—FastAPI, Spring Boot, ASP.NET—can auto-generate an OpenAPI spec from the code. If your team does not have one, that is a bigger problem than testing. Fix it first.
Validating Responses with Zod
Zod is a TypeScript-first schema validation library with 706 million monthly npm downloads. It compiles OpenAPI schemas into runtime validators. Here is how I use it inside Playwright:
import { z } from 'zod';
const UserSchema = z.object({
id: z.string().uuid(),
name: z.string().min(1),
email: z.string().email(),
role: z.enum(['admin', 'editor', 'viewer']),
createdAt: z.string().datetime(),
});
test('POST /api/users returns valid user contract', async ({ request }) => {
const res = await request.post('/api/users', {
data: { name: 'Pooja Nair', email: 'pooja@test.com', role: 'editor' }
});
expect(res.ok()).toBeTruthy();
const body = await res.json();
const parseResult = UserSchema.safeParse(body);
expect(parseResult.success, parseResult.error?.message).toBe(true);
});
If the backend team accidentally changes role from a string to an integer, safeParse fails with a descriptive error. The test breaks in CI before the frontend developer pulls the latest API build.
Auto-Generating Zod Schemas from OpenAPI
Hand-writing Zod schemas for 47 endpoints is tedious. I use openapi-zod-client in our build pipeline:
npx openapi-zod-client http://localhost:3000/api-json -o src/api/schemas.ts
This reads the live OpenAPI spec and generates Zod schemas plus typed request functions. When the backend deploys a breaking change, the generated schemas update, the TypeScript compiler complains, and the Playwright tests fail. It is a three-layer safety net: spec, types, and tests.
Cookie and Auth State Sharing Between API and Browser
The most powerful feature of Playwright’s hybrid pattern is authentication state sharing. You can log in via API, capture the cookies and local storage, and inject them into a new browser context. The UI test starts on an authenticated page without typing a password.
The Storage State Pattern
import { test as base, expect } from '@playwright/test';
// Create a custom fixture that logs in via API
export const test = base.extend({
authenticatedPage: async ({ page, request, browser }, use) => {
// Log in via API
const loginRes = await request.post('/api/auth/login', {
data: { email: 'test@tekion.com', password: process.env.TEST_PASSWORD }
});
expect(loginRes.ok()).toBeTruthy();
// Save storage state (cookies + localStorage)
const storageState = await request.storageState();
// Create new browser context with the saved state
const context = await browser.newContext({ storageState });
const authenticatedPage = await context.newPage();
await use(authenticatedPage);
await context.close();
},
});
test('admin sees billing page', async ({ authenticatedPage }) => {
await authenticatedPage.goto('/billing');
await expect(authenticatedPage.getByText('Invoices')).toBeVisible();
});
This pattern cuts an average of 6 seconds from every test that requires authentication. Over a 200-test suite, that is 20 minutes saved per run. In a CI pipeline running 30 builds per day, you recover 10 hours of compute time.
Context Request vs Global Request
Playwright has two request contexts:
- Global request (
playwright.request.newContext()): Isolated cookies. Use this when you want the API call to be independent of the browser. - Context request (
browserContext.request): Shared cookies. When the API response sets a cookie, the browser context automatically receives it. Use this when the API and UI must share the same session.
In most of my hybrid tests, I use the global request for setup (create user, create order) and the context request for assertions that need the browser’s session. Understanding the distinction prevents the most common auth bug I see in hybrid suites: tests that pass locally but fail in CI because the cookie domain does not match.
A Real Example: E-Commerce Checkout Flow
Here is a complete test from a project I consulted on last quarter. It tests the full checkout flow: create product via API, add to cart via UI, place order via UI, validate order via API.
import { test, expect } from '@playwright/test';
import { z } from 'zod';
const OrderSchema = z.object({
orderId: z.string().uuid(),
items: z.array(z.object({
productId: z.string(),
quantity: z.number().int().positive(),
price: z.number().positive()
})),
total: z.number().positive(),
status: z.enum(['pending', 'confirmed', 'shipped']),
createdAt: z.string().datetime()
});
test('checkout flow creates a valid order contract', async ({ page, request }) => {
// Step 1: Create product via API
const productRes = await request.post('/api/products', {
data: { name: 'Wireless Mouse', price: 1299, stock: 50 }
});
expect(productRes.ok()).toBeTruthy();
const product = await productRes.json();
// Step 2: Add to cart via UI
await page.goto(`/products/${product.id}`);
await page.getByRole('button', { name: 'Add to Cart' }).click();
await expect(page.getByText('Cart (1)')).toBeVisible();
// Step 3: Checkout via UI
await page.goto('/checkout');
await page.getByLabel('Full Name').fill('Ravi Kumar');
await page.getByLabel('Address').fill('42, MG Road, Bangalore');
await page.getByRole('button', { name: 'Place Order' }).click();
// Capture order ID from success page
await expect(page.getByText('Order Confirmed')).toBeVisible();
const orderId = await page.locator('[data-testid="order-id"]').textContent();
expect(orderId).toBeTruthy();
// Step 4: Validate order contract via API
const orderRes = await request.get(`/api/orders/${orderId}`);
expect(orderRes.ok()).toBeTruthy();
const order = await orderRes.json();
const parseResult = OrderSchema.safeParse(order);
expect(parseResult.success, parseResult.error?.message).toBe(true);
// Business assertions
expect(order.total).toBe(1299);
expect(order.status).toBe('confirmed');
expect(order.items).toHaveLength(1);
});
This single test validates four layers: API product creation, UI cart interaction, UI checkout flow, and API order contract. A bug in any layer breaks the test. A traditional suite would need three separate tests and three times the setup code.
CI/CD Integration: Docker and GitHub Actions
Hybrid tests need a running application. You cannot mock the API and test the UI against it in the same breath. I run the full stack in Docker Compose for local development and CI. I covered the unified Docker Compose setup in detail in Docker Compose Testing Setup: Playwright, Selenium Grid, and API.
The Minimal CI Pipeline
Here is the GitHub Actions workflow I use for hybrid suites:
name: Hybrid API + UI Tests
on: [push]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Start app and test services
run: docker compose -f docker-compose.test.yml up --abort-on-container-exit
env:
API_BASE_URL: http://app:3000
TEST_PASSWORD: ${{ secrets.TEST_PASSWORD }}
- name: Upload Playwright report
if: always()
uses: actions/upload-artifact@v4
with:
name: playwright-report
path: playwright-report/
The key flag is --abort-on-container-exit. If the Playwright tests fail, Docker Compose shuts down the app container immediately. Without this flag, the app keeps running, the CI job hangs, and you burn GitHub Actions minutes. I wrote about the full pipeline tuning in Playwright Docker GitHub Actions CI/CD Pipeline: The Complete 10-Minute Setup.
Parallel Execution Considerations
Hybrid tests are not independent by default. If Test A creates a product via API and Test B deletes all products via UI, they will interfere when run in parallel. I solve this with two patterns:
- Isolated test tenants: Each test worker gets a unique organization ID. All API calls and UI actions operate within that tenant. No collisions.
- Serial UI, parallel API: UI tests run serially (workers: 1) while API contract tests run in parallel (workers: 4). Playwright projects make this easy:
export default defineConfig({
projects: [
{ name: 'api-contract', testMatch: /api\\/.*.spec.ts/, workers: 4 },
{ name: 'hybrid-ui', testMatch: /hybrid\\/.*.spec.ts/, workers: 1 },
]
});
Common Traps When Mixing API and UI Tests
I have reviewed six hybrid suites in the last year. The same mistakes appear repeatedly.
Trap 1: Assuming API Success Means UI Stability
A green API test proves the endpoint returns the correct JSON. It does not prove the frontend handles that JSON correctly. I have seen cases where the API returns role: null and the contract test passes because null is a valid string in a loose Zod schema. The React component crashes because it calls .toUpperCase() on null. Always validate the edge cases in the UI step, not just the API step.
Trap 2: Hard-Coding Data That Conflicts Across Runs
Using email: 'test@test.com' in every test is a recipe for unique constraint violations. Generate unique data with libraries like faker-js or simple timestamps:
const email = `test-${Date.now()}@example.com`;
Trap 3: Ignoring API Errors in Setup Steps
When an API setup step fails, the error message is often buried in the test log. The UI test then fails with a timeout because the expected data does not exist. I wrap all API setup in a helper that throws descriptive errors:
async function createUser(request, data) {
const res = await request.post('/api/users', { data });
if (!res.ok()) {
const body = await res.text();
throw new Error(`User creation failed: ${res.status()} ${body}`);
}
return res.json();
}
Trap 4: Testing the API Contract in the UI Test
Some teams validate every field of an API response inside a UI test. This bloats the test and slows it down. Keep contract tests fast and separate. Use the hybrid pattern for flows that genuinely need both layers. Do not turn every UI test into an API test.
India Context: What Hiring Managers Pay for Hybrid Skills
In my SDET Salary Report India 2026, I showed that product companies pay 25-35 LPA for mid-level SDETs who can design hybrid test architectures. The specific skill that commands the premium is not Playwright alone. It is the ability to test below the UI.
The Skill Gap in Indian QA Market
Most service company QA engineers stop at UI automation. They know Selenium and TestNG. They can write a Page Object Model. But ask them to validate an OpenAPI schema or share cookie state between an API client and a browser, and you get blank stares. This is exactly the gap product companies are trying to fill.
A senior SDET at a Bangalore fintech startup told me last month: “I do not need someone who can click buttons. I need someone who can verify that our payment API contract is honored by both the mobile app and the web dashboard.” That engineer was hiring at 32 LPA fixed.
What to Show in Your Portfolio
If you are interviewing for a product company SDET role, do not show me a repo with 50 Selenium tests. Show me a hybrid suite where:
- API setup runs in under 1 second per test.
- OpenAPI schemas are validated with Zod or AJV.
- Auth state is shared between API and browser contexts.
- The CI pipeline runs the full suite in Docker in under 10 minutes.
That portfolio signal is worth more than any certification. I covered the exact interview questions I ask in SDET Interview Prep for AI-Era Hiring.
Key Takeaways
- API contract testing with Playwright combines REST validation and UI flows into one fast, reliable test.
- Playwright’s
APIRequestContexthandles HTTP requests, cookies, and headers natively. No external HTTP client needed. - OpenAPI schema validation with Zod (706M monthly downloads) catches backend breaking changes before they reach the frontend.
- Sharing auth state between API and browser contexts via
storageStatesaves 6+ seconds per authenticated test. - Hybrid tests run 3-4x faster than pure UI tests because data setup happens via API instead of form clicks.
- Keep API contract tests separate from UI tests when the flow does not need both. Use hybrid patterns for end-to-end validation only.
- In India, product companies pay 25-35 LPA for SDETs who can architect hybrid test suites with contract validation.
FAQ
Can I use Playwright API testing with Java or Python instead of TypeScript?
Yes. Playwright supports API testing in Java, Python, and .NET. The APIRequestContext is available in all languages. However, TypeScript has the best ecosystem for OpenAPI-to-Zod code generation, which makes contract validation smoother. For Python teams, I recommend pydantic instead of Zod for runtime schema checks.
Do I need a separate API testing tool if I use Playwright for hybrid tests?
No, but you might still want one. Playwright excels at request-level testing within a browser automation suite. If your team needs load testing, SOAP validation, or gRPC checks, keep a dedicated tool like k6 or REST Assured. For REST contract validation inside a UI suite, Playwright is sufficient.
How do I handle file uploads in hybrid tests?
Use Playwright’s request.post with a FormData payload for API file uploads, and page.setInputFiles for UI file uploads. The tricky part is sharing the uploaded file ID between the two. I store it in a test-scoped variable or fixture and pass it to the UI step.
What if my backend does not have an OpenAPI spec?
Generate one. Tools like fastapi, springdoc-openapi, and swashbuckle auto-generate specs from code. If your backend is legacy, use schemathesis or optic to infer the spec from traffic. A missing spec is not an excuse. It is a liability.
Are hybrid tests slower than pure API tests?
Yes, but only by the browser startup time. A pure API contract test takes 200-400ms. A hybrid test takes 2-4 seconds because it opens a browser. The trade-off is worth it when the flow needs UI validation. For pure contract checks, stick to API-only tests.
Can I mock the API in a hybrid test?
You can, but it defeats the purpose. Hybrid tests exist to validate the real integration between API and UI. If you mock the API, you are back to testing the frontend in isolation. Use Playwright’s page.route only for third-party dependencies, not your own backend.
