When a backend team renames a response field, E2E tests in staging catch it after deployment. Contract testing catches it before: the frontend generates a formal contract from its own tests, and the backend verifies it can satisfy those expectations independently, without running the full integration suite. This guide covers Pact for consumer-driven contract testing in a TypeScript stack, from generating a pact file to running provider verification in CI.

What contract testing is

Contract testing verifies that two services agree on the format of their communication, not just that each service works correctly on its own.

A contract is a formal description of what a consumer (your frontend or another service) expects from a provider (your API). The consumer generates the contract from its own tests. The provider verifies it can satisfy those expectations without running the consumer's full test suite.

The result: you catch breaking API changes before they reach staging.

Pact: the standard tool

Pact is the most widely adopted contract testing library. It works by recording real API interactions during consumer tests, saving them as "pacts" (JSON files), and replaying them against the real provider.

Install on the consumer side (frontend)

npm install -D @pact-foundation/pact

Write a consumer test

This test defines what your frontend expects from the /users/1 endpoint:

import { PactV3, MatchersV3 } from '@pact-foundation/pact';
import path from 'path';

const { like, string, integer } = MatchersV3;

const provider = new PactV3({
  consumer: 'frontend',
  provider: 'user-api',
  dir: path.resolve(process.cwd(), 'pacts'),
});

describe('User API contract', () => {
  it('returns user data', async () => {
    await provider
      .given('user 1 exists')
      .uponReceiving('a request for user 1')
      .withRequest({
        method: 'GET',
        path: '/users/1',
        headers: { Authorization: like('Bearer token123') },
      })
      .willRespondWith({
        status: 200,
        headers: { 'Content-Type': 'application/json' },
        body: {
          id: integer(1),
          name: string('Alice'),
          email: string('alice@example.com'),
        },
      })
      .executeTest(async (mockServer) => {
        const response = await fetch(`${mockServer.url}/users/1`, {
          headers: { Authorization: 'Bearer token123' },
        });
        const data = await response.json();

        expect(data.name).toBeDefined();
        expect(data.email).toBeDefined();
      });
  });
});

When this test runs, Pact:

1. Starts a mock server

2. Records the interaction (request + expected response) as a pact file in ./pacts/

3. Runs your test against the mock server

The generated pact file is the contract. It looks like this:

{
  "consumer": { "name": "frontend" },
  "provider": { "name": "user-api" },
  "interactions": [{
    "description": "a request for user 1",
    "request": { "method": "GET", "path": "/users/1" },
    "response": {
      "status": 200,
      "body": { "id": 1, "name": "Alice", "email": "alice@example.com" }
    }
  }]
}

Verify on the provider side (backend)

The backend team takes this pact file and verifies their API satisfies it:

import { Verifier } from '@pact-foundation/pact';

describe('Provider verification', () => {
  it('validates the expectations of Frontend', () => {
    return new Verifier({
      provider: 'user-api',
      providerBaseUrl: 'http://localhost:3001',
      pactUrls: [path.resolve(process.cwd(), 'pacts/frontend-user-api.json')],
      stateHandlers: {
        'user 1 exists': async () => {
          // seed test user into database
          await db.users.create({ id: 1, name: 'Alice', email: 'alice@example.com' });
        },
      },
    }).verifyProvider();
  });
});

The verifier sends the recorded request to the real backend and checks the response matches the contract. If the backend team renames name to full_name, this verification fails before any code ships to staging.

Where Playwright fits

Playwright handles E2E integration testing. Contract testing handles service-boundary verification. They complement each other.

In practice:

  • Contract tests catch: field renames, removed fields, changed status codes, type mismatches
  • Playwright E2E tests catch: UI rendering issues, user flow breakage, full-stack integration failures

You don't replace Playwright tests with contract tests. You use contract tests to make your Playwright tests faster and more stable, because fewer E2E failures come from API format changes that should have been caught earlier.

Pact Broker: sharing contracts across teams

When consumer and provider are in separate repos (common in microservices), you need a central place to store and share pact files. Pact Broker is the standard tool: open source, self-hostable, or available as PactFlow (their cloud service).

Teams publish pacts after consumer tests run. Provider pipelines pull the latest pact and verify before deploying. This creates a dependency check: the provider can't deploy if it breaks a consumer's contract.

When contract testing is worth it

Contract testing adds overhead: writing consumer tests in Pact's DSL, setting up provider state handlers, maintaining a broker. It pays off when:

  • You have multiple frontend apps or services calling the same API
  • API changes frequently break downstream consumers
  • You're in a microservices architecture where teams deploy independently
  • Your E2E suite is slow and you want to catch integration failures earlier

It's probably not worth it for:

  • A monolith where frontend and backend are in the same repo and deploy together
  • A team of two where both sides are the same person
  • An early-stage project where the API changes too frequently to maintain stable contracts

Start with Playwright for E2E. Add contract testing when you have two independently deployed services that break each other's assumptions.

→ See also: API Testing with Playwright's APIRequestContext (No Postman Required) | API Testing 101: What Every QA Engineer Needs to Know in 2026 | The Test Pyramid Explained for QA Engineers