A collection of utilities for Pact.js contract testing, designed to eliminate boilerplate and enforce best practices for consumer-driven contract testing.
📚 View the full documentation on GitHub Pages or browse the docs folder
- pactjs-utils
Pact.js is powerful but that power may come with friction for some teams. This library removes the repetitive parts so you can focus on writing contracts, not plumbing.
- JsonMap conversion headaches -- Pact expects
JsonMapfor provider state params, but your code uses typed objects.createProviderStateandtoJsonMaphandle the conversion so you never think about it. - Express middleware type gymnastics -- Injecting auth tokens into Pact verification requests requires wrestling with Express request/response types.
createRequestFilterwraps this into a single call with a pluggable token generator. - Scattered verifier configuration -- Building
VerifierOptionsmeans assembling broker URLs, consumer selectors, state handlers, request filters, and version tags from multiple sources.buildVerifierOptionsandbuildMessageVerifierOptionsconsolidate this into one configuration object. - Cross-execution bugs in CI -- When multiple provider-consumer pairs share a CI pipeline, getting consumer version selectors, breaking change flags, and webhook payloads right is error-prone.
handlePactBrokerUrlAndSelectorsreads environment variables and assembles the correct selector strategy automatically.
// pact/http/consumer/movies-read.pacttest.ts
import { createProviderState } from '@seontechnologies/pactjs-utils'
// Instead of manually converting to JsonMap:
const state = createProviderState({
name: 'Has a movie with a specific ID',
params: { id: 1, name: 'Inception', year: 2010 }
})
// Use with pact.addInteraction().given():
await pact.addInteraction().given(...state)// pact/http/provider/provider-contract.pacttest.ts
import { buildVerifierOptions } from '@seontechnologies/pactjs-utils'
const options = buildVerifierOptions({
provider: 'SampleMoviesAPI',
port: '3001',
stateHandlers,
includeMainAndDeployed: process.env.PACT_BREAKING_CHANGE !== 'true',
requestFilter: createRequestFilter({
tokenGenerator: () => generateAuthToken({ userIdentifier: 'admin' })
})
})
const verifier = new Verifier(options)
await verifier.verifyProvider()// pact/http/helpers/pact-helpers.ts
import { createRequestFilter } from '@seontechnologies/pactjs-utils'
// Custom token generator
const filter = createRequestFilter({
tokenGenerator: () =>
`${new Date().toISOString()}:${JSON.stringify({ userId: 'admin' })}`
})
// Default (ISO timestamp token)
const defaultFilter = createRequestFilter()
// No auth needed
import { noOpRequestFilter } from '@seontechnologies/pactjs-utils'// pact/message/provider/provider-message-queue.pacttest.ts
import { buildMessageVerifierOptions } from '@seontechnologies/pactjs-utils'
const options = buildMessageVerifierOptions({
provider: 'SampleMoviesAPI-event-producer',
messageProviders,
stateHandlers,
includeMainAndDeployed: true
})
const messagePact = new MessageProviderPact(options)
await messagePact.verify()npm i -D @seontechnologies/pactjs-utilsNote: This package requires
@pact-foundation/pact >= 16.2.0as a peer dependency.
| Category | Utilities | Docs |
|---|---|---|
| Consumer | toJsonMap, createProviderState |
Consumer Helpers |
| Schema | zodToPactMatchers |
Zod to Pact |
| Provider | buildVerifierOptions, buildMessageVerifierOptions, handlePactBrokerUrlAndSelectors, getProviderVersionTags |
Provider Verifier |
| Auth | createRequestFilter, noOpRequestFilter |
Request Filter |
Pact supports three types of contract testing. This project implements all three:
| Type | Status | Cost | Local | Directory |
|---|---|---|---|---|
| Consumer-Driven (HTTP) | Implemented | Free | Yes | pact/http/ |
| Message Queue (Kafka/async) | Implemented | Free | Yes | pact/message/ |
| Bi-Directional (provider-driven) | Implemented | PactFlow req | No | scripts/ (OAS) |
Consumer-Driven Contract Testing (CDCT) -- The consumer defines HTTP interaction expectations, generates pact files, and the provider verifies against them. This is the core Pact workflow. Supports both local and broker modes. (pact/http/consumer/ for consumer tests, pact/http/provider/ for provider verification)
Message Queue Testing -- The same consumer-driven approach applied to async messaging (Kafka, SQS, etc.). The consumer defines message expectations, and the provider verifies it can produce matching messages using MessageProviderPact. Supports both local and broker modes. (pact/message/consumer/ for consumer tests, pact/message/provider/ for provider verification)
Bi-Directional Contract Testing (BDCT) -- The provider publishes an auto-generated OpenAPI spec to PactFlow, which cross-validates it against consumer pacts. No local mode -- requires PactFlow to perform the cross-validation. The provider's own tests (unit/integration) serve as the verification evidence. (scripts/publish-provider-contract.sh)
pact/
http/ # HTTP contract tests (CDCT)
consumer/ # Consumer-driven HTTP expectations
movies-read.pacttest.ts # GET operations
movies-write.pacttest.ts # POST/PUT/DELETE operations
provider/ # HTTP provider verification
provider-contract.pacttest.ts # Verification (local + broker)
helpers/ # HTTP-specific helpers
pact-helpers.ts # Auth token generation
state-handlers.ts # Provider state setup (DB seeding)
vitest.consumer.config.mts # HTTP consumer test config
vitest.provider.config.mts # HTTP provider test config
message/ # Message queue contract tests (Kafka/async)
consumer/ # Consumer-side message expectations
movie-events.pacttest.ts # Movie event message expectations
provider/ # Message provider verification
provider-message-queue.pacttest.ts # Verification (local + broker)
helpers/ # Message-specific helpers
message-providers.ts # Kafka message producers
state-handlers.ts # Re-exports HTTP state handlers
vitest.consumer.config.mts # Message consumer test config
vitest.provider.config.mts # Message provider test config
scripts/
publish-provider-contract.sh # Bi-directional: publish OAS to PactFlow (PAID)
sample-app/backend/src/api-docs/
openapi.json # Auto-generated OpenAPI 3.1.0 spec (from Zod schemas)
There are three flows: local (no broker, for development), remote (with Pact Broker, for CI/CD), and provider-driven (bi-directional, publishes OAS to PactFlow).
The local flow is self-contained. This can be used in monorepos, similar to the repo where the sample front & backe ends coexist. The consumer generates pact files, and the provider verifies directly against those local files. No broker, no network, no environment variables needed.
# 1. Run consumer pact tests — generates pact JSON files in pacts/
npm run test:pact:consumer
# 2. Run provider verification against local pact files
npm run test:pact:provider:local # Verify CDCT+MQ locally
npm run test:pact:provider:local:contract # (optional - only execute CDCT)
npm run test:pact:provider:local:message # (optional - only execute message queue)
# Or run both in sequence:
npm run test:pact:localThe provider reads the pact files from the pacts/ directory using pactUrls instead of pulling from a broker. This is the fastest way to iterate during development.
The remote flow uses a Pact Broker (e.g. PactFlow) to coordinate contracts between repositories. This is how contract testing works in CI/CD with separate consumer and provider repos.
Setup: Create a .env file (see .env.example):
PACT_BROKER_TOKEN=your_token_here
PACT_BROKER_BASE_URL=https://your-org.pactflow.ioConsumer flow:
npm run test:pact:consumer # (1) Generate pact files
# commit before publishing — publish tags the version with GITHUB_SHA
npm run publish:pact # (2) Publish to broker
npm run can:i:deploy:consumer # (4) Check deployment safety
# only on main:
npm run record:consumer:deployment # (5) Record deployment to environmentProvider flow:
npm run test:pact:provider:remote # (3) Verify CDCT+MQ against broker (triggered by webhook)
npm run test:pact:provider:remote:contract # (optional - only execute CDCT)
npm run test:pact:provider:remote:message # (optional - only execute message queue)
npm run can:i:deploy:provider # (4) Check deployment safety
# only on main:
npm run record:provider:deployment # (5) Record deployment to environmentThe key coordination: when there are separate repos, the provider must run verification (3) after the consumer publishes (2) but before the consumer can check can-i-deploy (4). Webhooks automate this trigger -- when the consumer publishes a new pact, the broker fires a webhook that triggers the provider's CI to run verification.
sequenceDiagram
participant C as Consumer Repo
participant B as Pact Broker
participant P as Provider Repo
C->>C: test:pact:consumer
C->>C: git commit
C->>B: publish:pact
Note right of B: Stores contract
B-->>P: Webhook: contract_requiring_verification_published\n(PACT_PAYLOAD_URL)
P->>P: test:pact:provider:remote
C->>B: can:i:deploy:consumer
P->>B: can:i:deploy:provider
C->>B: record:consumer:deployment
P->>B: record:provider:deployment
Environment variables used in remote flow:
| Variable | Purpose | Default |
|---|---|---|
PACT_BROKER_BASE_URL |
Pact Broker URL | (required) |
PACT_BROKER_TOKEN |
Auth token for broker | (required) |
GITHUB_SHA |
Provider/consumer version (commit hash) | 'unknown' |
GITHUB_BRANCH |
Provider version branch | 'main' |
PACT_PAYLOAD_URL |
Direct pact URL (set by webhooks) | --- |
PACT_BREAKING_CHANGE |
Skip mainBranch/deployedOrReleased selectors | 'false' |
Consumer version selectors (controlled by includeMainAndDeployed param):
| Selector | When included | Purpose |
|---|---|---|
matchingBranch |
Always | Verify against consumer's matching feature branch |
mainBranch |
includeMainAndDeployed |
Verify against consumer's main branch |
deployedOrReleased |
includeMainAndDeployed |
Verify against consumer's deployed versions |
Breaking changes: When the provider introduces a breaking change, set includeMainAndDeployed: false (or PACT_BREAKING_CHANGE=true in CI) to only verify against matching branches. This prevents failures from incompatible mainBranch or deployedOrReleased consumer versions while coordinated PRs are in flight.
# Provider PR with breaking change:
PACT_BREAKING_CHANGE=true npm run test:pact:provider:remoteBi-directional contract testing flips the model: instead of running the Pact verifier against a live provider, both sides publish independently to PactFlow, which cross-validates the consumer pacts against the provider's OpenAPI spec.
No local mode. BDCT requires PactFlow to perform the cross-validation -- there is no way to run this locally. The consumer-driven flow covers local development needs.
Consumer flow:
npm run test:pact:consumer # (1) Generate pact files
# commit before publishing — publish tags the version with GITHUB_SHA
npm run publish:pact # (2) Publish consumer pacts to PactFlow
npm run can:i:deploy:consumer # (4) Check deployment safety
# only on main:
npm run record:consumer:deployment # (5) Record deployment to environmentProvider flow:
npm run publish:provider:contract # (3) Generate OAS, run tests, publish to PactFlow
npm run can:i:deploy:provider # (4) Check deployment safety
# only on main:
npm run record:provider:deployment # (5) Record deployment to environmentThe key difference from CDCT: step (3) publishes the provider's OpenAPI spec instead of running the Pact verifier against a live provider. PactFlow cross-validates the consumer pacts against the OAS automatically -- no webhooks needed. Both sides can publish independently.
What publish:provider:contract does under the hood in this sample repo:
- Generates a fresh OpenAPI spec from Zod schemas (
openapi.json) - Runs
npm run test:backendand captures the exit code (usually this would be an api e2e suite that does e2e + schema validation, ensuring that the OpenAPI spec is flawless per actual network data) - Publishes the OAS + verification results via
pactflow publish-provider-contract
Each workflow is a standalone file that can be copied to its own repo. The monorepo only matters for local development (npm run test:pact:local).
| Workflow | Repo | Trigger | Purpose |
|---|---|---|---|
contract-test-consumer.yml |
Consumer | PR + push to main | Generate pacts, publish, can-i-deploy |
contract-test-provider.yml |
Provider | PR + push to main | Verify HTTP pacts, can-i-deploy |
contract-test-provider-message.yml |
Provider | PR + push to main | Verify message pacts, can-i-deploy |
contract-test-webhook.yml |
Provider | PactFlow webhook | Verify when consumer publishes new pact |
contract-test-publish-openapi.yml |
Provider | Push to main + manual | Publish OAS to PactFlow (BDCT) |
contract-test-local.yml |
Monorepo | PR + manual | Local consumer + provider (no broker) |
Breaking change detection: All workflows parse the PR description for a [x] Pact breaking change checkbox. When checked, can-i-deploy is skipped and provider verification only runs against matching branches. Add the checkbox via the PR template (.github/PULL_REQUEST_TEMPLATE.md).
Webhook setup (one-time): PactFlow must be configured to fire a GitHub repository_dispatch event when a consumer publishes a new pact. This triggers contract-test-webhook.yml in the provider repo. You can either:
- Create the webhook manually in PactFlow UI (Settings > Webhooks) — see
scripts/one-time-scripts/create-pact-webhook.shfor the exact field values - Run the script directly with the
pact-brokerCLI after filling in your tokens
Both approaches require a GitHub PAT with public_repo scope, stored as a PactFlow secret named githubToken. Use scripts/one-time-scripts/create-github-issue-test.sh to verify your PAT works before creating the webhook.
Required GitHub secrets:
| Secret | Purpose |
|---|---|
PACT_BROKER_BASE_URL |
PactFlow broker URL |
PACT_BROKER_TOKEN |
PactFlow API token |
npm run test:lib # library unit tests (vitest)
npm run test:backend # sample-app backend tests
npm run test:frontend # sample-app frontend tests
npm run test:pact:consumer # all consumer contract tests (HTTP + message)
npm run test:pact:consumer:http # consumer HTTP contract tests
npm run test:pact:consumer:message # consumer message contract tests
npm run test:pact:provider:local # provider CDCT+MQ verification (local)
npm run test:pact:provider:local:contract # provider CDCT verification (local)
npm run test:pact:provider:local:message # provider message verification (local)
npm run test:pact:local # full local flow (consumer + provider)
npm run test:pact:provider:remote # provider CDCT+MQ verification (broker)
npm run test:pact:provider:remote:contract # provider CDCT verification (broker)
npm run test:pact:provider:remote:message # provider message verification (broker)nvm use
npm install
npm run validate # typecheck + lint + test + format
npm run test:lib # library unit tests
npm run test:backend # sample-app backend tests
npm run test:frontend # sample-app frontend tests
npm run build # CJS + ESM + typesThis package supports both CommonJS and ES Modules:
// Works in both CJS and ESM
import { buildVerifierOptions } from '@seontechnologies/pactjs-utils'# Build the package
npm run build
# Create a tarball package
npm pack
# Install in a target repository (change the version according to the file name)
# For npm projects:
npm install ../playwright-utils/seontechnologies-playwright-utils-1.0.1.tgz
# For pnpm projects:
pnpm add file:/path/to/playwright-utils-1.0.1.tgzThis package is published to the public npm registry under the @seontechnologies scope.
You can trigger a release directly from GitHub's web interface:
- Go to the repository → Actions → "Publish Package" workflow
- Click "Run workflow" button (dropdown on the right)
- Select options in the form:
- Branch: main
- Version type: patch/minor/major/custom
- Custom version: Only needed if you selected "custom" type
- Click "Run workflow"
Important:
- Requires
NPM_TOKENsecret to be configured in GitHub repository settings - You must review and merge the PR to complete the process
You can also publish the package locally using the provided script:
# 1. Set your npm token as an environment variable
# Get your token from: https://www.npmjs.com/settings/~/tokens
export NPM_TOKEN=your_npm_token
# 2. Run the publish script
npm run publish:localSee CONTRIBUTING.md for setup, development workflow, and pull request guidelines.
Licensed under the terms of the LICENSE file.