API Contract Testing with Pact.js (2026 Guide)
Written by Kajal · Reviewed and published by Prasandeep

Modern systems rarely ship as one deployable. A frontend calls one service, a mobile app another, and microservices exchange data across teams and release trains. That flexibility creates a common failure mode: one service changes, another breaks without anyone noticing until integration or production.
API contract testing makes integration expectations explicit, testable, and safe to evolve. Pact.js is the JavaScript/Node implementation of Pact: the consumer defines what it needs, Pact records a contract, and the provider proves it still satisfies that interaction—usually in CI, before deploy.
This guide is Pact.js–focused (Jest/Mocha, frontend BFFs, Node microservices). For broker governance, JVM examples, async messaging, and a four-week rollout across the portfolio, see Contract Testing for Microservices: The 2026 Definitive Guide. For where contracts sit in the pyramid, see Modern Test Pyramid 2026: Complete Strategy.
What contract testing means
Contract testing validates the agreement between two systems: HTTP method, path, query parameters, headers, status code, and response body shape. The goal is not to test every behavior of an API—only that the consumer’s real expectations are still met by the provider.
Boundaries drift: fields rename, date formats change, wrappers appear, 200 becomes 204. Changes that look harmless on the provider side can break consumers instantly. Contracts turn those assumptions into executable checks based on actual usage, not static docs alone.
Why Pact.js is valuable
Pact.js implements consumer-driven contract testing for Node.js, frontends, and TypeScript-heavy stacks.
| Strength | What it gives you |
|---|---|
| Consumer-centric | Only interactions the consumer uses belong in the contract |
| Focused artifacts | Smaller, maintainable pacts vs broad integration suites |
| Independent deploys | Consumer and provider ship on different cadences when verification stays green |
| Fast CI feedback | Catch breaking API changes without full environment orchestration |
Pact.js fits when:
- A frontend or BFF depends on backend APIs
- Multiple microservices are owned by different teams
- You want integration confidence without spinning up every dependency per PR
- Your stack is JavaScript/TypeScript end to end
It narrows the gap between what teams believe an API guarantees and what it actually guarantees at the boundary.
Consumer-driven contract testing
The consumer defines expectations; the provider verifies them. That differs from provider-only OpenAPI checks that may not reflect what callers actually consume.
Typical workflow:
- Consumer test describes the request it will make.
- Consumer test describes the response it expects.
- Pact records the interaction as a pact file (JSON contract).
- Provider runs verification against that pact (real HTTP to a running or test instance).
You avoid over-testing every response variant and under-testing assumptions that no longer hold. If the consumer only needs id, name, and email, those fields matter; the provider may add optional fields later. If email disappears or changes type, verification fails before users do.
How Pact.js fits in a real workflow
Two sides, two repos or one monorepo—same pattern:
| Side | Responsibility |
|---|---|
| Consumer | Mock interaction during test → write pact file |
| Provider | Load pact → exercise API → assert match |
The consumer team publishes expectations; the provider team proves it can still meet them. Failure blocks deploy before a full integrated environment surfaces the break.
Consumer tests → pact.json → (broker optional) → Provider verify job
A simple use case
A frontend loads profile data with GET /users/123 and expects JSON with id, name, and email.
Without contracts, the UI team may only learn the backend changed after deploy. With Pact.js, the frontend test defines that interaction; if the provider drops email or changes types, provider verification fails in CI.
Pact protects real consumer behavior, not abstract API documentation.
Step 1 — Setting up Pact.js
Install dependencies in the consumer project:
npm install --save-dev @pact-foundation/pactUse Jest or Mocha as your test runner. Configure output directory for pact files (often pacts/).
Consumer test flow:
- Create a Pact mock provider (
PactV3). - Define expected request and response (use matchers for volatile fields).
- Run consumer code against the mock server URL Pact provides.
- Assert consumer behavior; Pact writes the contract on success.
Provider verification flow:
- Load pact file(s) from consumer or broker.
- Start provider API (or point at test instance).
- For each interaction: replay request, compare response to contract.
- Fail CI on mismatch.
Start with one consumer–provider pair; expand after both sides trust the workflow. Rollout patterns: Contract Testing for Microservices: The 2026 Definitive Guide.
Step 2 — Consumer test example
Practical consumer-side test for a user profile API:
const { PactV3, MatchersV3 } = require('@pact-foundation/pact');
const { like, eachLike } = MatchersV3;
describe('User service consumer', () => {
const provider = new PactV3({
consumer: 'FrontendApp',
provider: 'UserService',
dir: './pacts',
logLevel: 'warn',
});
it('returns user profile details', async () => {
await provider
.given('user 123 exists')
.uponReceiving('a request for user 123')
.withRequest({
method: 'GET',
path: '/users/123',
})
.willRespondWith({
status: 200,
headers: {
'Content-Type': 'application/json',
},
body: {
id: like(123),
name: like('John Doe'),
email: like('john.doe@example.com'),
},
})
.executeTest(async (mockServer) => {
const response = await fetch(`${mockServer.url}/users/123`);
const body = await response.json();
expect(response.status).toBe(200);
expect(body.id).toBe(123);
expect(body.name).toBe('John Doe');
expect(body.email).toBe('john.doe@example.com');
});
});
});This test does two jobs: validates consumer code against a mock, and generates the pact the real provider must satisfy. The consumer is not testing the full provider—only the interaction it depends on.
Step 3 — Provider verification
Provider verification runs against the real service (local, container, or deployed test env):
// provider/verify.pact.test.js — illustrative; align with your server bootstrap.
const { Verifier } = require('@pact-foundation/pact');
const path = require('path');
describe('UserService provider verification', () => {
it('honors FrontendApp contracts', async () => {
const opts = {
provider: 'UserService',
providerBaseUrl: process.env.USER_SERVICE_URL || 'http://localhost:4000',
pactUrls: [path.resolve(__dirname, '../pacts/FrontendApp-UserService.json')],
stateHandlers: {
'user 123 exists': async () => {
// Seed DB or stub so GET /users/123 returns profile data
},
},
};
await new Verifier(opts).verifyProvider();
});
});Verification checks method, path, query, headers, status, and body structure. Consumer expects 200 but provider returns 404 → failure. Consumer expects email but provider omits it → failure.
Provider teams can refactor internals freely; public response shape at the contract boundary is what Pact guards.
Matchers and flexible data
Hardcoding timestamps, UUIDs, and tokens makes contracts fragile. Pact matchers assert type and shape instead of exact runtime values.
| Matcher idea | Use when |
|---|---|
like(123) | Numeric id with example value |
like('john.doe@example.com') | String with representative sample |
| Regex or ISO datetime matchers | createdAt, tokens |
Contracts should protect compatibility, not punish normal variation. Matchers keep pacts stable while allowing realistic provider responses.
What Pact.js is best for
Strong fit:
- Frontends and BFFs depending on backend APIs
- Service-to-service calls in Node microservices
- Mobile clients on internal or public APIs
- Platform APIs shared by multiple teams
- JS/TS organizations with distributed ownership
High integration-break cost + need for faster feedback than full-stack environments → Pact.js earns its place at service seams.
What Pact.js does not replace
Pact is not a substitute for every test type:
| Layer | Role |
|---|---|
| Unit | Business logic inside one service |
| Contract (Pact) | Consumer–provider compatibility at the boundary |
| Integration | Broader wiring with more dependencies |
| End-to-end | Full user journeys across systems |
| Performance / security | Load, authz, abuse cases |
Contract tests sit between unit and E2E: excellent for interfaces, insufficient for “does the whole checkout work under real load?” Layer deliberately: Risk-Based Testing Framework for Enterprise Teams.
Common mistakes teams make
- Over-contracting — Encoding every response field creates brittle pacts. Keep contracts to what the consumer truly needs.
- Too-strict assertions — Hardcode only what must be exact; use matchers for dynamic data.
- Skipping provider verification — Consumer tests alone do not protect production. Providers must verify—or contracts drift silently.
- Treating Pact like E2E — Pact validates compatibility, not full workflows. Keep journey tests separate.
- Hiding contracts — Contracts help when both teams see them—in PRs, broker UI, or shared artifacts. Buried files in one repo lose shared-truth value.
Pact.js in CI/CD
Typical pipeline:
- Run consumer tests → generate pact files.
- Publish pacts to a broker or artifact store (tagged by branch/version).
- Run provider verification (fetch relevant pacts).
- Fail the build on compatibility break.
- Optional: can-i-deploy gate before promoting to staging/production.
Illustrative GitHub Actions split:
# .github/workflows/pact-consumer.yml
name: pact-consumer
on: [pull_request]
jobs:
consumer:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: '20' }
- run: npm ci
- run: npm test -- --testPathPattern=pact
- name: Publish pacts
if: github.ref == 'refs/heads/main'
run: npx pact-broker publish ./pacts --consumer-app-version "${{ github.sha }}"
env:
PACT_BROKER_BASE_URL: ${{ secrets.PACT_BROKER_BASE_URL }}
PACT_BROKER_TOKEN: ${{ secrets.PACT_BROKER_TOKEN }}# .github/workflows/pact-provider.yml
name: pact-provider
on: [pull_request, workflow_dispatch]
jobs:
verify:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: '20' }
- run: npm ci
- run: npm run start:test & # provider under test
- run: npm run pact:verify
env:
PACT_BROKER_BASE_URL: ${{ secrets.PACT_BROKER_BASE_URL }}
PACT_BROKER_TOKEN: ${{ secrets.PACT_BROKER_TOKEN }}Broader CI patterns: GitHub Actions + Playwright CI/CD pipeline—same principles apply to contract jobs (artifacts, gates, fast feedback).
Pact Broker and shared contracts
At scale, pact files cross many repos. A Pact Broker stores, versions, and distributes contracts.
- Consumers publish after tests pass.
- Providers fetch relevant consumer versions and verify in CI.
- Teams see who depends on whom, verification history, and whether a provider change is safe to release.
Broker visibility is especially valuable when dozens of services and multiple consumers exist per API.
When to use Pact.js versus other approaches
| Question | Best tool |
|---|---|
| Will this service still satisfy apps that depend on it? | Pact.js |
| Does full checkout work with all systems running? | E2E (plus contracts) |
| Is internal business logic correct? | Unit tests |
| Can the system handle load? | Performance testing |
| Java stack, same problem | Pact JVM + REST Assured for API depth |
Layered strategy keeps teams fast and safe; Pact targets the boundary where microservice regressions cluster.
Practical advice for teams
Introducing Pact.js:
- Pick one consumer–provider pair with a real pain point (recent production break helps).
- Write one contract for a critical interaction.
- Make the provider verify it in CI.
- Expand after both sides see a caught break or a safe refactor.
Readable contracts communicate intent: a reviewer should see what the consumer expects and why.
Conclusion
API contract testing with Pact.js is one of the most practical ways to prevent broken integrations in JavaScript systems. It turns compatibility into fast, repeatable CI checks and makes expectations explicit for consumers and providers.
Technical value plus collaboration: teams evolve services independently while honoring published contracts. Pact.js will not replace unit, integration, or end-to-end testing—but it makes seams between services much safer.
If your stack includes frontends, Node services, or multi-team microservices, add Pact.js to your strategy and treat the definitive microservices guide as the next step for broker hardening and portfolio rollout.