Resource library

QA How-To

API contract testing with Pact: A Practical Guide (2026)

Learn API contract testing with Pact using runnable consumer and provider examples, matching rules, provider states, broker workflows, CI gates, and adoption.

25 min read | 3,389 words

TL;DR

API contract testing with Pact lets a consumer record its real HTTP expectations as a versioned contract, then lets the provider replay and verify those interactions before either side deploys. Use the current Pact JS V4 interface, flexible matchers, deterministic provider states, and a broker-backed CI workflow. Test the actual client, not a hand-written request that duplicates it.

Key Takeaways

  • A Pact consumer test must exercise the real client code and capture only behavior the consumer actually relies on.
  • Matching rules express meaningful flexibility, while exact examples alone create brittle contracts.
  • Provider verification replays every relevant interaction against a running provider in deterministic provider states.
  • A Pact Broker adds versioned contract exchange, verification results, deployment checks, and traceability across services.
  • Provider states describe preconditions, not imperative test scripts, and their handlers must be repeatable and isolated.
  • Contract tests complement provider unit, schema, integration, and end-to-end tests rather than replacing them.
  • Adopt Pact one high-value consumer-provider relationship at a time and make ownership part of the API change process.

API contract testing with Pact catches integration-breaking API changes without requiring every consumer and provider to run together in one shared environment. The consumer tests its real API client against a Pact mock server, publishes the generated interaction contract, and the provider verifies that contract against its running implementation. The result is fast feedback about whether independently released service versions can communicate.

Pact is consumer-driven, so it records what a specific consumer needs rather than every capability an API offers. That focus is powerful, but it also requires disciplined matching rules, provider states, version metadata, and ownership. This guide uses the current Pact JS V4 interface and shows how to build a useful workflow rather than a collection of JSON files.

TL;DR

Stage Owner Artifact or result Failure means
Consumer test Consumer team Pact interaction file The real client did not make the declared request or handle the example response
Publish Consumer CI Versioned pact in a broker Contract exchange or version metadata is incomplete
Provider verification Provider team Verification result for a provider version The provider cannot satisfy a consumer interaction
Deployment check Release pipeline Compatibility decision Required contract verification is missing or failed

A minimal flow is consumer test -> publish pact -> provider verification -> publish verification -> deployment check. Keep broader behavior, data integrity, resilience, and end-to-end coverage in their appropriate test layers.

1. What API Contract Testing with Pact Actually Proves

A contract test proves that a consumer's observed request can be accepted by a provider and that the provider's response satisfies the shapes and values the consumer requires. An interaction includes a provider state, a descriptive request expectation, and an expected response with matching rules. Pact first enforces the interaction from the consumer side with a mock provider. Later, a verifier sends that request to the real provider and compares the actual response with the same rules.

This is narrower than proving a business journey works. Pact does not show that the provider wrote the correct database row, emitted every side effect, handled load, or integrated with a third party. Provider tests must cover internal behavior. End-to-end tests can cover a few critical deployed journeys. Pact's job is the seam: HTTP method, path, query, relevant headers, body, status, and response contract used by one consumer.

The consumer is the source of demand. If a mobile app reads id, status, and total, its contract should not freeze twenty unused response fields. If a dashboard depends on a particular error status and error code, that behavior belongs in its pact even if another consumer does not care. Multiple consumers can therefore publish different valid expectations for the same provider.

Pact works best where teams own independently deployable services or clients and need quick change feedback. For a small monolith compiled and deployed as one unit, an in-process integration test may be simpler. Use API testing strategy and test pyramid to place contract tests alongside unit, component, integration, and end-to-end coverage.

2. Compare Contracts, Schemas, Mocks, and Integration Tests

Teams often call several different techniques contract testing. An OpenAPI schema describes the provider's intended interface, often from the provider perspective. A Pact file records examples and matching rules produced from a consumer test. A static mock returns configured examples but may never be checked against the provider. An integration test calls a deployed dependency and includes network and environment variables. Each answers a different question.

Technique Primary question Strength Blind spot
Pact consumer-driven contract Can this provider version satisfy this consumer version's observed needs? Executable on both sides, version-aware Does not prove provider internals or full journeys
OpenAPI validation Does traffic conform to a declared general schema? Broad operation and documentation coverage A valid provider schema may still break a specific consumer assumption
Static mock server Can development proceed against representative examples? Fast and convenient Mock behavior can drift from production
Provider component test Does the provider behave correctly with controlled dependencies? Deep business and persistence assertions May not encode actual consumer usage
End-to-end test Does a deployed user journey work across systems? Broad confidence in a real topology Slower, harder to diagnose, often less deterministic

Pact and OpenAPI can coexist. OpenAPI can drive documentation, server validation, and broad conformance. Pact can show which portions and variations are actively required by named consumers. Do not mechanically transform a complete provider specification into a consumer pact. That reverses ownership and creates a contract that says nothing about real client behavior.

A Pact mock is also not a general service virtualization environment. It starts for the test, accepts only the configured interaction, verifies the expected request occurred, and writes a pact after success. This strictness is the feature. A permissive mock that accepts any request can make a broken client appear healthy.

3. Set Up Pact JS and a Real API Client

Use a supported Node.js release and pin dependencies with the repository lockfile. For a TypeScript project, install Pact and your test runner with npm install --save-dev @pact-foundation/pact vitest typescript. Pact JS 16 aliases the V4 Pact interface as Pact and exposes current matchers as Matchers. Setting the specification explicitly prevents serialization intent from being accidental.

The production client should accept a base URL so a test can point it at Pact's dynamically allocated mock server. It should also own HTTP behavior such as headers, error mapping, and deserialization. Here is a runnable client using the standard fetch API available in supported Node runtimes:

// src/orders-client.ts
export interface Order {
  id: number;
  status: 'pending' | 'paid' | 'cancelled';
  total: number;
}

export async function getOrder(
  baseUrl: string,
  orderId: number
): Promise<Order> {
  const response = await fetch(`${baseUrl}/orders/${orderId}`, {
    headers: { Accept: 'application/json' }
  });

  if (!response.ok) {
    throw new Error(`Order API returned ${response.status}`);
  }

  return await response.json() as Order;
}

Do not add a pactMode branch to production code. Dependency injection of a base URL is ordinary testability. The consumer test should call getOrder, not issue a second hand-written fetch that merely resembles it. Otherwise a developer can change the real client's path or header while the Pact test remains green.

Keep pact output in a clean, dedicated directory. Old interactions left in that directory can be republished and create false expectations. Clean before the consumer contract suite, but do not let parallel jobs delete a directory another job is using. Give each process isolated output or serialize generation for a consumer-provider pair.

4. Write a Consumer Test with Pact V4

The consumer test arranges a provider state and interaction, runs the real client against mockServer.url, and asserts the client's behavior. Pact additionally confirms that the request matched the configured method, path, headers, and other constraints. On success, it updates the pact file in the configured directory.

// tests/orders.pact.test.ts
import path from 'node:path';
import { describe, expect, test } from 'vitest';
import {
  Matchers,
  Pact,
  SpecificationVersion
} from '@pact-foundation/pact';
import { getOrder } from '../src/orders-client';

const { decimal, integer, string } = Matchers;

const pact = new Pact({
  consumer: 'CheckoutWeb',
  provider: 'OrdersAPI',
  dir: path.resolve(process.cwd(), 'pacts'),
  spec: SpecificationVersion.SPECIFICATION_VERSION_V4
});

describe('Orders API contract', () => {
  test('returns an existing paid order', async () => {
    await pact
      .addInteraction()
      .given('order 42 exists and is paid')
      .uponReceiving('a request for order 42')
      .withRequest('GET', '/orders/42', (builder) => {
        builder.headers({ Accept: 'application/json' });
      })
      .willRespondWith(200, (builder) => {
        builder.headers({ 'Content-Type': 'application/json' });
        builder.jsonBody({
          id: integer(42),
          status: string('paid'),
          total: decimal(79.5)
        });
      })
      .executeTest(async (mockServer) => {
        const order = await getOrder(mockServer.url, 42);

        expect(order.id).toBe(42);
        expect(order.status).toBe('paid');
        expect(order.total).toBe(79.5);
      });
  });
});

The examples are valid concrete values, while matchers communicate where other values are acceptable during provider verification. The exact path matters because the consumer constructs it. The Accept header matters because the client sends it. Avoid adding headers that an HTTP library generates differently across environments unless the consumer truly depends on them.

The provider state describes a business precondition, not how to create it. The consumer team can say order 42 exists and is paid; the provider team decides whether its verifier handler inserts a row, configures an in-memory repository, or stubs a downstream port. That separation keeps the contract from reaching into provider internals.

5. Design Matching Rules Without Making Contracts Weak

Matchers are the difference between an executable requirement and a frozen example. Exact matching is appropriate for meaningful constants such as an error code, content type family, or requested resource ID. Type matchers are appropriate when the consumer accepts any value of that type. Regular expressions are useful for a real format constraint, but they should not become a substitute for a clear semantic expectation.

Suppose a consumer displays an order's identifier, state, and total. It may accept any integer ID, one of a defined set of status strings, and any decimal total. If the consumer specifically branches on status === 'paid', an interaction returning paid is meaningful. A generic string matcher alone would permit provider verification to pass with unknown, even though the client type and behavior reject it. Model separate interactions for behaviorally distinct variants.

Arrays require careful thought. eachLike says elements follow a structure, but minimum size and empty-list behavior are separate consumer cases. A consumer that renders an empty state needs an explicit empty response interaction. A consumer that reads the first element requires at least one element. Contract the assumption instead of hoping a representative list implies it.

Avoid over-specification. Exact timestamps, generated IDs, response header order, all provider fields, and incidental whitespace make verification brittle without protecting consumer behavior. Avoid under-specification too. Matching an entire response with a broad like can miss an enum or absence rule the client depends on. Review each field with one question: what change here would actually break this consumer?

For deeper negative assertions around status codes and error bodies, pair this workflow with API error handling and negative testing. Negative consumer interactions are valuable when the client has real behavior for not found, unauthorized, validation, conflict, or rate-limit responses.

6. Verify the Provider and Implement Provider States

Provider verification starts the provider locally with controlled dependencies, loads relevant pacts, invokes state handlers, replays each request, and evaluates the actual response against the pact rules. It should run in the provider repository so failures are close to the code change. External dependencies should be replaced at clear ports when possible, making states fast and deterministic without bypassing the provider's routing, validation, serialization, and business logic.

The following verifier assumes the Orders API is already listening on port 8080 and a test repository is available to the test process. The local file path makes the first proof of concept easy. A mature workflow retrieves pacts from a broker.

// tests/provider.pact.test.ts
import path from 'node:path';
import { describe, test } from 'vitest';
import { Verifier } from '@pact-foundation/pact';
import { orderRepository } from '../src/order-repository';

describe('Orders API provider verification', () => {
  test('satisfies the CheckoutWeb pact', async () => {
    const verifier = new Verifier({
      providerBaseUrl: 'http://127.0.0.1:8080',
      pactUrls: [
        path.resolve(process.cwd(), 'pacts/CheckoutWeb-OrdersAPI.json')
      ],
      stateHandlers: {
        'order 42 exists and is paid': async () => {
          await orderRepository.replaceAll([
            { id: 42, status: 'paid', total: 79.5 }
          ]);
          return { id: 42 };
        }
      }
    });

    await verifier.verifyProvider();
  });
});

replaceAll is application repository code shown as an explicit test seam, not a Pact method. In your system, use a documented fixture API, test database transaction, or in-memory adapter. State setup must be idempotent because the verifier can invoke interactions in an order you should not depend on. If two interactions share data, each state still prepares everything it needs.

Authentication tokens that cannot be stored in a pact can be added through a verifier request filter, but use filters sparingly. A filter that changes the path, query, body, or meaningful headers can cause verification to test a request different from the consumer contract. Prefer a provider state that prepares authorization and a filter that only supplies a short-lived credential.

7. Use a Pact Broker and CI Deployment Gates

Passing pact files through repository commits, shared folders, or build artifacts can demonstrate the mechanics, but it does not answer version compatibility at scale. A Pact Broker stores pacts by consumer version, verification results by provider version, branches or tags, and deployment or release information. That graph lets a pipeline ask whether a specific application version is safe to deploy into an environment.

The consumer pipeline should run unit and contract tests, publish pacts using an immutable consumer application version such as the commit SHA, and record the branch. The provider pipeline should select relevant consumer versions, verify their pacts, and publish results using the provider commit SHA. A deployment pipeline should run can-i-deploy or the current broker deployment check for the exact version and target environment. After deployment, record that version as deployed so later decisions use real environment state.

Do not publish developer-local verification as authoritative. In Pact JS, publishVerificationResult should be true only in CI and requires a provider version. Protect broker tokens, and never echo them in logs. Use branch-aware selectors rather than relying indefinitely on broad legacy tags. Mobile consumers need special attention because multiple app versions can remain active in production; provider verification must cover the supported population, not just one latest consumer.

Pending and work-in-progress pact workflows can prevent a new consumer expectation from immediately breaking the provider's main build while still producing visible verification feedback. They are collaboration mechanisms, not permission to leave incompatibility unresolved. Define an owner and service-level expectation for adopting pending changes.

See CI/CD testing pipeline design for the surrounding quality gates, artifact flow, and failure ownership. Contract compatibility should be one explicit gate, not a hidden side effect of a general test job.

8. Test Errors, Optional Fields, and Version Changes

Happy-path contracts are rarely enough. Add interactions for response variants the consumer deliberately handles. A missing order might require status 404 with a stable error code. A stale update might require 409. A validation failure might require field-level details. The contract should assert only the error fields the client reads and the statuses that drive behavior. It should not freeze a provider's complete diagnostic payload.

Absence and null are different. If a consumer can handle an optional property being absent but not null, include behavior that demonstrates the accepted variant. Do not assume a type matcher automatically explains optionality. Similarly, an empty array, missing array, and null array should be treated according to client behavior. Strong client types help expose which cases need contracts.

For additive provider changes, a well-designed consumer pact usually keeps passing because it does not prohibit unused response fields. Removing or changing a required field should fail provider verification before deployment. Consumer request changes create a new pact that the provider must verify. Coordinate genuinely breaking semantic changes through API versioning or a staged expand-and-contract rollout. Pact detects compatibility; it does not design the migration for you.

Pact V4 also supports asynchronous and synchronous message interactions. Messaging contracts need the same discipline about actual consumer code, metadata, matching rules, and provider verification. They do not prove broker delivery guarantees, ordering under concurrency, retry policy, or end-to-end processing. Keep transport and resilience tests separate.

9. Make the Suite Maintainable and Trustworthy

Organize tests around consumer use cases, not provider endpoints alone. Names should explain why the client makes the request and what behavior it expects. One interaction should remain understandable during a provider failure. Large generated pacts with hundreds of near-duplicate variants are expensive to review and may indicate data-driven tests are freezing irrelevant combinations.

Assign ownership on both sides. The consumer owns the truthfulness of its tests and removes obsolete pacts when behavior disappears. The provider owns deterministic state handlers and timely verification. Platform owners may provide broker infrastructure and CI templates, but they cannot decide whether a field is behaviorally required. API change review should include the broker compatibility state and affected consumer teams.

Measure useful outcomes rather than pact count. Track unverified contract changes, age of pending interactions, failed deployment checks, provider verification duration, and incidents that escaped because a consumer assumption was absent. A rising interaction count is not automatically better coverage. One carefully designed interaction can protect more value than ten generated snapshots.

Keep provider verification close to component-test speed. Slow state handlers that call remote systems recreate the shared-environment problem Pact is designed to reduce. Use controlled test doubles behind the provider, but do not mock the HTTP boundary or serializer being verified. A provider test that replaces its own route handler proves nothing.

Review generated pact diffs as build artifacts. Unexpected removed interactions, renamed consumers, changed paths, or exact-value expansions deserve attention before publication. Never hand-edit the pact JSON. Change the consumer test and regenerate it so the executable source remains authoritative.

10. Adopt API Contract Testing with Pact Incrementally

Start with one consumer-provider relationship that changes often, has clear ownership, and has caused integration regressions. Select two or three meaningful interactions, including one error response. Run consumer generation and provider verification locally from clean checkouts. Seed a breaking provider change and confirm verification fails for the reason you expect. This proves the feedback loop before infrastructure work grows.

Next, deploy a broker, define immutable version identifiers, configure branch-aware selection, and publish verification only from CI. Add the deployment check in advisory mode first so teams can see missing metadata and workflow mistakes. Once the signal is reliable and ownership is documented, make the check blocking. Record deployments or releases so the compatibility graph reflects reality.

Expand by risk, not by endpoint inventory. Prioritize shared providers, frequently released consumers, expensive staging dependencies, and high-impact error behavior. Provide templates for Pact construction, matching conventions, provider states, and broker environment variables. Keep templates thin enough that teams still understand the underlying lifecycle.

Finally, run a quarterly contract health review. Remove obsolete consumer versions according to product support policy, resolve stale pending pacts, simplify duplicate states, upgrade SDKs deliberately, and rehearse broker recovery. Pact creates value through a living collaboration workflow. Unowned JSON accumulated in a broker is not a safety net.

Interview Questions and Answers

Q: What does consumer-driven contract testing mean?

The consumer writes an executable test that records the requests it makes and the response elements it needs. The provider then verifies those interactions against its real HTTP implementation. This focuses compatibility on observed consumer behavior rather than the provider's entire possible schema.

Q: Why must a Pact test call the real client?

If the test sends a separate hand-written HTTP request, it can stay green while production client code changes incorrectly. Calling the real client makes path construction, headers, serialization, error mapping, and response use part of the test. Pact then verifies that actual outbound behavior.

Q: What are provider states?

Provider states are declarative preconditions such as order 42 exists. The consumer names the state, and the provider owns a handler that prepares deterministic data or dependency behavior. State handlers must be isolated, repeatable, and independent of interaction order.

Q: How are Pact matching rules different from examples?

An example is a concrete value the mock server can return. A matcher describes which actual provider values remain compatible, such as any integer or a string matching a format. Good contracts use exact values for business constants and flexible matchers where variation is safe.

Q: Does Pact replace integration or end-to-end tests?

No. Pact verifies compatibility at a service boundary. It does not prove database side effects, downstream integration, performance, security, delivery guarantees, or full user journeys, so those need other test layers.

Q: What does a Pact Broker add?

A broker stores versioned pacts and provider verification results and relates them to branches and deployed or released versions. CI can then evaluate whether a particular consumer or provider version is compatible with the versions in a target environment. It also makes ownership and verification status visible.

Q: How do you avoid brittle Pact contracts?

Contract only fields and values the consumer uses, choose matchers deliberately, avoid generated or incidental headers, and model behaviorally distinct variants separately. Review pact diffs and remove obsolete interactions. Do not generate pacts mechanically from a provider schema.

Q: How would you introduce Pact into an existing architecture?

I would start with one painful integration and a few high-value interactions, prove a seeded breaking change is caught, and then add broker publishing and provider verification. Deployment checks begin advisory and become blocking after version and environment metadata are reliable. Expansion follows risk and ownership.

Common Mistakes

  • Testing a hand-written request instead of the production consumer client.
  • Treating a Pact file as provider-owned API documentation.
  • Matching every value exactly, including timestamps, generated IDs, and irrelevant headers.
  • Using broad matchers that permit values the consumer cannot actually handle.
  • Writing provider states as ordered scripts or letting them depend on shared remote data.
  • Hand-editing generated pact JSON rather than changing the consumer test.
  • Publishing pacts without immutable consumer versions and branch metadata.
  • Publishing local verification results as though they came from provider CI.
  • Making a deployment check blocking before deployment records and selectors are trustworthy.
  • Assuming contract tests cover persistence, authorization policy, performance, or complete user journeys.

Conclusion

API contract testing with Pact gives independently delivered consumers and providers a fast, executable compatibility check. Its value comes from testing the real client, expressing precise matching rules, replaying contracts against deterministic provider states, and making versioned verification part of the release decision.

Begin with one relationship and one known risk. Prove that an incompatible change fails before deployment, then build the broker and ownership workflow around that signal. Keep the contracts small, behavior-driven, and actively maintained.

Interview Questions and Answers

Explain the Pact contract testing lifecycle.

The consumer tests its real client against a Pact mock and generates a contract. Consumer CI publishes that pact under an immutable application version. Provider CI retrieves relevant pacts, prepares provider states, verifies requests against the running provider, and publishes results. The release pipeline checks compatibility for the exact version and environment.

Why is Pact called consumer-driven?

The consumer specifies only the interactions and response data it relies on. This prevents a provider's entire schema from becoming a frozen requirement for every consumer. The provider remains responsible for proving that its implementation satisfies all active consumer contracts.

What is the purpose of matching rules in Pact?

Matchers separate a representative example from the set of compatible provider values. They reduce brittleness for safely variable data while retaining exact checks for meaningful constants. Matcher choice should reflect what would actually break consumer behavior.

How do provider states work?

A consumer interaction names a declarative precondition. During verification, the provider maps that name to setup code for controlled data or dependencies. Handlers should be idempotent, isolated, and independent of interaction order.

Why should Pact tests execute production client code?

That makes request construction, serialization, headers, error handling, and response consumption observable. A duplicate hand-written request can pass even when the real client is broken. Testing the actual collaborator keeps the contract aligned with consumer behavior.

What does can-i-deploy protect against?

It evaluates whether verification results exist and pass for a specific application version against versions relevant to the target environment. It prevents deployment when compatibility is failed or unknown. Its accuracy depends on immutable versioning and correctly recorded deployments or releases.

What does Pact not test?

It does not prove provider internal correctness, persistence side effects, load behavior, security policy, third-party availability, or complete user journeys. It also does not prove messaging delivery guarantees merely by matching message content. Those belong to other test layers.

How do you keep Pact tests maintainable?

I organize interactions by consumer behavior, match only fields the client uses, keep provider states deterministic, review generated diffs, and remove obsolete contracts. I track pending changes and verification duration rather than rewarding raw interaction count. Ownership remains with both consumer and provider teams.

Frequently Asked Questions

What is API contract testing with Pact?

It is a consumer-driven workflow where a consumer test records the HTTP interactions it relies on and the provider replays those interactions against its implementation. Pact compares requests and responses with explicit matching rules. A broker can connect the results to application versions and deployments.

Is Pact the same as OpenAPI validation?

No. OpenAPI usually describes a provider's broad intended interface, while Pact captures needs observed through a specific consumer's executable test. They complement each other and can identify different compatibility gaps.

Should Pact tests call a real test environment?

Consumer tests call Pact's local mock server, and provider verification calls a locally running provider with controlled dependencies. Shared remote environments usually make the feedback slower and less deterministic. A small separate end-to-end suite can still validate deployed journeys.

What should go in a Pact provider state?

Use a declarative business precondition such as `customer 10 has no orders`. The provider implements repeatable setup behind that name. Do not put database commands or provider implementation details in the consumer contract.

Can Pact test error responses?

Yes. Add interactions for errors the consumer intentionally handles, such as 404, 409, or validation responses. Assert the status and only the stable error fields that drive client behavior.

Do I need a Pact Broker?

A local pact file is enough to learn and prove the interaction. A broker becomes important for multiple teams because it versions contracts and verification results, supports selectors, and enables compatibility checks before deployment.

Does Pact replace end-to-end testing?

No. Pact verifies service-boundary compatibility, while end-to-end tests verify selected deployed journeys. Provider business behavior, persistence, security, resilience, performance, and messaging guarantees also need appropriate separate tests.

Related Guides