Est.

Contract Testing With Pact for REST API Integration

Pact lets services verify they're compatible without needing to run simultaneously.

Senior Staff Writer · · 10 min read
Cover illustration for “Contract Testing With Pact for REST API Integration”
Integration Testing · October 7, 2026 · 10 min read · 2,232 words

Contract testing exists because end-to-end integration testing stops working once a service graph grows past a handful of nodes. A single test of the interaction between service A and service B is never just that interaction. Because the provider holds state, service A often needs to put that state in place before the test can run, so a single check turns into a multi-step scenario with several points of failure stacked in sequence.

Each additional step in a scenario adds its own chance of flaking, and because the steps run in sequence, one broken link fails the entire chain. Past 50 services, the problem stops being just a testing problem. Coordinated deployment across that many services, database seeding that has to land in a consistent state across all of them, and external dependencies that need to behave predictably all have to hold at once, and in practice they fail in combinations nobody designed for or anticipated.

None of this argues for testing less. It argues for testing differently: verifying that two services agree on the shape of their interaction without requiring both of them to be running, seeded, and stable at the same moment. That's the specific door Pact walks through, and understanding the mechanism it uses to do that is what the rest of this piece covers.

What Pact does: the consumer-driven contract model

Pact flips the usual order of API design. A consumer test runs against a mock server that Pact manages, and that mock server records the actual requests the consumer sends and the actual responses the consumer parses. The result is a pact file: a JSON artifact, readable by machines, that captures those recorded interactions in full.

For a REST API, that file holds the HTTP method, the path, the headers, the status code, and matchers describing the shape of the response body. Pact's matcher DSL gives a consumer several ways to express that shape:

| Matcher | What it checks | |---|---| | like | the value matches the given type, not the exact value | | eachLike | an array where every element follows a given shape | | regex | the value matches a given pattern | | integer | the value is a whole number | | decimal | the value is a floating-point number | | boolean | the value is true or false | | uuid | the value is a UUID-formatted string | | timestamp | the value is a date-time formatted string |

A contract written with discipline describes only the behavior the consumer actually relies on. A contract that asserts every field a provider happens to return couples the consumer to details it never touches, and it breaks the build the moment the provider changes a field nobody downstream was using. That kind of over-assertion is one of the more common reasons teams get frustrated with contract testing early on, because the contracts were written too broadly.

The same model extends past plain REST. Pact v4 goes further still, adding support for gRPC and Protobuf through its Plugin Framework. One organization can run the same consumer-driven approach across REST, GraphQL, and gRPC services rather than maintaining separate testing philosophies for each protocol.

None of this makes the pact file documentation. It's an executable artifact, checked by the provider on every build, and that distinction is what makes the rest of the workflow possible.

The five-step workflow from consumer test to verified compatibility

Diagram: Pact's Five-Step Workflow: Consumer Test to Verified Compatibility. Visualizes: Show a linear five-step flow that traces how Pact closes the gap between consumer expectations and provider behaviour without both services running…

Pact's full lifecycle runs in five steps, and together they close the gap between what a consumer expects and what a provider actually does, without requiring the two services to run at the same time in the same environment.

The first step belongs to the consumer. A test defines its interactions using .given() to declare the provider state it depends on, .uponReceiving() to describe the request it will send, and .willRespondWith() to describe the response it expects back. Pact's library spins up a local mock server for the duration of the test and records the exchange as it happens.

The second step is automatic: the library takes the recorded interactions and serializes them into a versioned JSON file, dropped into a local pacts/ directory alongside the rest of the test output.

Third, that file gets published to the Pact Broker, a shared service that stores contracts and verification results for every consumer and provider in the organization.

Fourth, the provider verifies the contract. It pulls the consumer's contracts from the broker and runs them against its own real implementation using Pact's Verifier. A Node.js verifier configuration might look roughly like this:

new Verifier({
provider: 'PaymentService',
providerBaseUrl: ',
pactUrls: ['],
stateHandlers: {
'user exists': () => db.seed({ user: testUser }),
'account has sufficient balance': () => db.seed({ balance: 10000 })
},
publishVerificationResult: true,
providerVersion: process.env.GIT_SHA
}).verifyProvider();

Once verification finishes, the result is published back to the broker along with the provider's version, again tied to the git SHA.

Fifth, can-i-deploy gates the release. The broker's can-i-deploy tool checks whether a specific consumer version and a specific provider version have been verified as compatible, and whether that compatibility holds against whatever versions are already live in the target environment. The asymmetry built into all five steps is deliberate: the consumer proves it has encoded its expectations correctly, the provider proves it satisfies them, and at no point does either side need the other running in order to test its own half.

Versioning and tagging discipline that makes can-i-deploy trustworthy

can-i-deploy only tells the truth if the version data feeding it is clean, and sloppy versioning makes the gate lie worse than having no gate. The rule that matters most is simple to state and easy to violate under deadline pressure: the application version published to the broker should always be, or clearly contain, the git SHA. A version string that gets reused across two different builds means two different states of the code share one verification record, and the broker has no way to tell them apart afterward.

This failure is most visible once a team has been running Pact successfully for a while, once the broker holds a real history and the compatibility matrix has grown dense enough that one ambiguous entry can quietly poison decisions for every service connected to it. Each one should be versioned alongside the API it describes and tagged with the provider version it was checked against, and a contract should stay in the broker until every consumer depending on it has moved off that version, not a moment sooner.

Skipping this discipline produces a failure mode that surfaces quietly. A provider can ship a change that breaks a consumer, and the broker will still show that change as verified, because the verification that ran was checked against a stale contract carrying an ambiguous version tag. The gate looks green. The underlying compatibility it's supposed to represent no longer exists.

Provider state handlers: where statefulness enters the Pact model

Provider state handlers are how Pact deals with the fact that most interesting API responses depend on what's sitting in a database at the moment of the call. Rather than requiring a shared environment pre-seeded with every condition every consumer might need, each interaction in a contract declares the precondition it requires, and the provider's own test suite builds that condition when verification runs.

The mechanism is direct. A consumer writes a state string, something like .given('user exists'), and the provider maps that string to a handler function that inserts the matching record into its own test database before the interaction gets checked. That mapping resolves before verification proceeds, so the provider is always testing against a known, deliberately constructed state rather than whatever happened to be left over from a previous run.

Because of this, provider verification is self-contained. It needs no shared staging database, no consumer running anywhere, and no live external service, only the provider's own code and its own test database. Shared mutable state between verifications is what produces flaky provider test runs, and a flaky provider test quietly undermines the whole point of the contract, since nobody can trust a signal that fails for reasons unrelated to compatibility.

If a state handler needs to call a live third-party API just to set up its precondition, the contract boundary has been drawn in the wrong place. The handler is reaching outside the system it's supposed to verify and pulling in a dependency that was never meant to be part of an isolated check. That's exactly the seam where Pact's model, built for services a team controls end to end, runs into its limit.

Pact's Mock Server and the Stateless Simulation Gap

Pact's mock server does one job, and it does it precisely: it maps a request shape to a response shape and returns that response every time it sees a matching request, with no memory of anything that happened earlier in the same test session. Checking the shape of an interaction is the entire point of this kind of tool.

The limitation is visible in any multi-step flow. Any consistency between those two responses exists because a developer configured both of them to agree, not because the mock enforces any relationship between calls.

This matters most for workflows with several dependent steps. Provider states solve this problem on the provider's side of verification, where a real database enforces real consistency. They do nothing for the consumer side during development, where the consumer is testing its own logic against a mock that cannot catch state inconsistencies the consumer's own code introduces.

The gap opens widest exactly where a team has the least control: third-party APIs. The consumer is testing solely against its own mock server, and that mock reflects only what a developer typed into it, nothing about what the real service does today or will do after its next release.

The Contract Testing Model and Third-Party APIs: The BDCT Approach

Classic Pact assumes the provider will write and run verification tests against the contract, and that assumption holds fine inside an organization that owns both sides of an integration.

Bi-Directional Contract Testing, offered through PactFlow, is built for exactly that situation. The provider uploads its OpenAPI specification to PactFlow, and PactFlow checks that specification against the consumer's pact file automatically, with no provider-side test code required. What changes is where the compatibility check happens, at the level of the declared spec rather than through a live verification suite running against real provider code.

That shift is also BDCT's limit. A spec is a description of an API, not the API itself, and the two can drift apart without anyone noticing until a consumer hits the difference in production.

That drift isn't hypothetical. A GitHub issue opened October 6, 2026 on the vercel-labs/emulate repository documents a Slack emulator missing the channel_type and event_ts fields that GenericMessageEvent requires. A spec-level check would miss exactly this kind of gap, because a spec can declare those fields present even while the running emulator never sends them. The same technique does not extend to Stripe's actual production API, and Stripe's own documentation reflects that reality directly: it recommends simulating Stripe's output and request shapes with mock data for day-to-day testing, and reserving real calls to Stripe's API for infrequent validation runs, specifically to avoid rate limits.

Stateful simulation as the complement Pact needs for third-party API testing

Everything Pact leaves unresolved for third-party integrations, no memory of state across calls, no provider willing to run verification, drift risk between a spec and the real behavior behind it, points toward a stateful simulator that gets checked against the real API on an ongoing basis.

Rystic's simulators cover services including Slack, GitHub, Stripe, PayPal, Kalshi, Lob, and Resend, and before every release, the same requests get sent to both the live API and the simulator, with the agreement rate published alongside each one, 93.9% for Slack, 97.9% for GitHub, 91.7% for Kalshi, and 100.0% for PayPal, Lob, and Okta among the figures listed. A simulator checked this way reflects what the real API does now instead of drifting quietly out of step with it the way a one-time spec upload can.

The scaling pressure that makes end-to-end testing break down across an internal service graph does not disappear at the edge of an organization's own infrastructure. Stateful simulators are built to close precisely that opening, giving a consumer the same kind of state-management guarantee Pact's provider states offer internally, but pointed at a service nobody on the team can modify.

Rystic's simulators are pre-seeded with realistic, cohesive data rather than handed to a developer as an empty sandbox requiring its own setup scripts before it says anything useful, and each one remembers state the way the real service would: add a customer, list customers, and the new one is there; delete it, and it's gone from the list on the next call. That's the same behavior a payment flow needs when it creates a charge, confirms it, and retrieves it in sequence, the exact multi-step pattern where Pact's stateless mock server offers no guarantee. For a team integrating a third-party API, running that kind of simulator locally or in CI means testing against something that behaves like the real service across a whole sequence of calls while avoiding live credentials, staying clear of rate limits, and skipping the wait on a provider that was never going to write Pact verification code.

Diagram: Third-Party API Agreement Rates: Simulator vs. Live API. Visualizes: Show a ranked bar or stat-callout display of the published agreement rates between Rystic's simulators and their corresponding live APIs, as a concrete measure of…

More in Integration Testing