Contributing to pactjs-utils
April 28, 2026 ยท View on GitHub
Welcome to @seontechnologies/pactjs-utils. This library provides reusable utilities for Pact.js consumer-driven contract testing, reducing boilerplate and enforcing consistent patterns across provider verification, consumer test setup, and request filtering.
Project Vision
Pact.js contract testing involves significant configuration boilerplate: wiring up broker URLs, consumer version selectors, request filters, state handlers, and environment-aware defaults. This library extracts those patterns into well-tested, composable utilities so that teams can focus on writing contract tests rather than configuring infrastructure.
Getting Started
Prerequisites
- Node.js 20+ (the
.nvmrcfile specifies version 24) - npm
@pact-foundation/pact >= 16.2.0(peer dependency)
Initial Setup
git clone https://github.com/seontechnologies/pactjs-utils.git
cd pactjs-utils
nvm use
npm install
Verify Your Setup
npm run validate
This runs typecheck, lint, tests, and formatting in parallel. If it passes, your environment is ready.
Build the Library
npm run build
This cleans the dist/ directory and produces three outputs: CommonJS (dist/cjs/), ES Modules (dist/esm/), and TypeScript declarations (dist/types/).
Development Workflow
- Run
nvm useto ensure you are on the correct Node.js version. - Run
npm installto install dependencies. - Make your changes following the architecture and design principles below.
- Run
npm run validateto verify everything passes (typecheck, lint, test, format). - Run
npm run buildto confirm the library builds cleanly. - Submit a pull request following the PR guidelines.
Architecture and Design Principles
Functional Core
The library favors pure functions with no side effects in core utilities. Functions like toJsonMap and createProviderState take input and return output without mutating external state.
// src/consumer-helpers/to-json-map.ts
export const toJsonMap = (obj: Record<string, unknown>): JsonMap =>
Object.fromEntries(
Object.entries(obj).map(([key, value]) => {
// Pure transformation logic, no side effects
})
)
Higher-Order Functions for Customizable Behavior
Where behavior needs to be configurable, the library uses higher-order functions that return configured implementations. The createRequestFilter function is the primary example: it accepts an optional tokenGenerator and returns a RequestFilter function.
// src/request-filter/create-request-filter.ts
export const createRequestFilter =
(options?: RequestFilterOptions): RequestFilter =>
(req, _, next) => {
const tokenGenerator = options?.tokenGenerator || defaultTokenGenerator
// Adds Bearer authorization header if not present
}
Configuration Builder Pattern
Complex configuration objects (like Pact VerifierOptions) are constructed through builder functions that encapsulate defaults, environment variable resolution, and conditional logic. buildVerifierOptions and buildMessageVerifierOptions demonstrate this pattern.
// Usage example
const options = buildVerifierOptions({
provider: 'SampleMoviesAPI',
port: '3001',
stateHandlers,
includeMainAndDeployed: true
})
Environment-Aware Defaults with Explicit Overrides
Functions accept explicit parameter values but fall back to environment variables when not provided. This allows local development with sensible defaults while supporting CI/CD injection of secrets and metadata.
// Parameters default to env vars but accept explicit overrides
buildVerifierOptions({
provider: 'SampleMoviesAPI',
port: '3001',
includeMainAndDeployed: true
// These all have env var fallbacks:
// pactBrokerToken defaults to process.env.PACT_BROKER_TOKEN
// providerVersion defaults to process.env.GITHUB_SHA || 'unknown'
// providerVersionBranch defaults to process.env.GITHUB_BRANCH || 'main'
// pactBrokerUrl defaults to process.env.PACT_BROKER_BASE_URL
})
Module Structure
The library source code lives in src/ and is organized into focused modules:
src/
index.ts # Barrel export for all public APIs
pact-types.ts # Shared type definitions (JsonMap, StateHandlers, RequestFilter, etc.)
consumer-helpers/
index.ts # Module barrel export
create-provider-state.ts # Creates provider state tuples for consumer tests
create-provider-state.test.ts
to-json-map.ts # Converts arbitrary objects to Pact-compatible JsonMap
to-json-map.test.ts
request-filter/
index.ts # Module barrel export
create-request-filter.ts # Higher-order function for authorization request filters
create-request-filter.test.ts
provider-verifier/
index.ts # Module barrel export
build-verifier-options.ts # Builds VerifierOptions and PactMessageProviderOptions
build-verifier-options.test.ts
handle-url-and-selectors.ts # Pact broker URL and consumer version selector logic
handle-url-and-selectors.test.ts
Module Descriptions
- consumer-helpers: Utilities for consumer-side Pact tests.
createProviderStateproduces tuples forprovider.given(...)calls.toJsonMapconverts arbitrary objects into Pact-compatible JSON maps. - request-filter: Provides
createRequestFilter(a higher-order function that returns an Express-compatible request filter for adding authorization headers) andnoOpRequestFilter(a pass-through filter). - provider-verifier: Builds complete
VerifierOptionsandPactMessageProviderOptionsobjects for provider verification. Handles Pact Broker URL resolution, consumer version selectors, payload URL matching, and provider version tagging. - pact-types: Local type definitions mirroring internal
@pact-foundation/pacttypes that are not re-exported from the main package entry point. This avoids deep internal imports that could break across Pact updates.
Adding New Utilities
Module Structure
Create a new directory under src/ with the following structure:
src/new-utility/
index.ts # Module barrel export
new-utility.ts # Implementation
new-utility.test.ts # Co-located tests
Implementation Checklist
- Create the implementation file with pure, well-typed functions.
- Create an
index.tsbarrel export that re-exports public functions and types. - Write co-located tests in a
*.test.tsfile alongside the implementation. - Add exports to
src/index.tsso consumers can import from the library root. - Add types to
src/pact-types.tsif the new utility introduces shared Pact-related types. - Run
npm run validateto confirm everything passes.
Example: Adding a New Utility
// src/new-utility/my-helper.ts
import type { JsonMap } from '../pact-types'
export const myHelper = (input: string): JsonMap => {
// Implementation
}
// src/new-utility/index.ts
export { myHelper } from './my-helper'
// src/new-utility/my-helper.test.ts
import { describe, it, expect } from 'vitest'
import { myHelper } from './my-helper'
describe('myHelper', () => {
it('should transform input correctly', () => {
const result = myHelper('test')
expect(result).toEqual({
/* expected */
})
})
})
Then add to src/index.ts:
export { myHelper } from './new-utility'
Code Standards
TypeScript
- Strict mode is enabled (
strict: true,strictNullChecks: true,strictFunctionTypes: true,noUncheckedIndexedAccess: true). - Prefer
typeoverinterfacefor type definitions. - Use explicit return types for exported functions.
- Avoid
any; useunknownand proper generics instead. - Avoid enums; use
as constobjects or union types.
File Organization
- Source files use kebab-case (e.g.,
create-request-filter.ts,build-verifier-options.ts). - Each module has an
index.tsbarrel export. - Tests are co-located with their source files using the
*.test.tsnaming pattern. - Keep files under 250 lines. Break large files into smaller, focused functions.
Functional Style
- Prefer pure functions without side effects.
- Avoid mutations and global state.
- Use higher-order functions for configurable behavior.
- Keep functions small and focused.
Testing Requirements
Test Framework
The library uses Vitest for all tests. The configuration is in vitest.config.mts:
export default defineConfig({
test: {
environment: 'node',
include: ['src/**/*.test.ts']
}
})
Test Standards
- Co-located tests: Test files live alongside their source files with a
*.test.tssuffix. - Mock isolation: Use
vi.fn()andvi.mock()for test doubles. Do not rely on external services or shared mutable state. - Explicit assertions: Keep assertions in test bodies, not abstracted into helpers.
- Deterministic: Tests must not depend on execution order, external timing, or shared global state.
- Self-contained: Each test sets up its own data and does not depend on other tests.
Test Patterns in This Codebase
Tests follow a consistent pattern. Here is an example from the request filter tests:
import { describe, it, expect, vi } from 'vitest'
import { createRequestFilter, noOpRequestFilter } from './create-request-filter'
const makeReq = (headers: Record<string, string> = {}) =>
({ headers: { ...headers }, body: undefined }) as never
describe('createRequestFilter', () => {
it('adds a Bearer authorization header when absent', () => {
const filter = createRequestFilter()!
const req = makeReq()
const result = filter(req, {} as never, undefined as never)
expect(result).toBeDefined()
expect(
(req as { headers: Record<string, string> }).headers['authorization']
).toMatch(/^Bearer .+/)
})
it('uses a custom tokenGenerator', () => {
const filter = createRequestFilter({
tokenGenerator: () => 'my-custom-token'
})!
const req = makeReq()
filter(req, {} as never, undefined as never)
expect(
(req as { headers: Record<string, string> }).headers['authorization']
).toBe('Bearer my-custom-token')
})
})
Running Tests
# Run library unit tests
npm run test:lib
# Run all tests (library + sample app backend + sample app frontend)
npm run test
# Run sample app Pact consumer tests
npm run test:pact:consumer
# Run sample app Pact provider tests (local CDCT + MQ)
npm run test:pact:provider:local
npm run test:pact:provider:local:contract # optional - CDCT only
npm run test:pact:provider:local:message # optional - message only
# Run both consumer and provider Pact tests locally
npm run test:pact:local
Environment Configuration
The .env.example file documents the available environment variables:
| Variable | Purpose | Required |
|---|---|---|
PACT_BROKER_BASE_URL | Pact Broker / PactFlow base URL | For remote verification |
PACT_BROKER_TOKEN | Authentication token for the Pact Broker | For remote verification |
GITHUB_SHA | Git commit SHA (set automatically in CI) | For provider versioning |
GITHUB_BRANCH | Git branch name (set automatically in CI) | For provider versioning |
PACT_BREAKING_CHANGE | Set to true during coordinated breaking changes | Optional |
PACT_PAYLOAD_URL | Webhook payload URL (set by PactFlow webhooks) | Automatic |
For local development, the library provides sensible fallbacks (e.g., providerVersion defaults to 'unknown', providerVersionBranch defaults to 'main', and version tags default to ['local']).
Submitting Changes
Pull Request Guidelines
- Keep PRs focused: One feature or fix per PR.
- Size limit: Aim for fewer than 500 lines changed. If larger, explain why.
- Tests required: All new code must have co-located
*.test.tstests. - CI must pass: All checks in
npm run validatemust be green. - No breaking changes without documentation and a major version bump plan.
PR Checklist
- [ ] New code follows functional style (pure functions, no unnecessary side effects)
- [ ] Co-located tests added with \*.test.ts naming
- [ ] All tests pass (`npm run test:lib`)
- [ ] Typecheck passes (`npm run typecheck`)
- [ ] Lint passes (`npm run lint`)
- [ ] `npm run validate` passes
- [ ] Build succeeds (`npm run build`)
- [ ] Exports added to src/index.ts if introducing new public APIs
- [ ] Types added to src/pact-types.ts if introducing new shared Pact types
- [ ] No breaking change, or breaking change is documented
- [ ] No secrets or credentials committed
Commit Message Format
type: brief description
Detailed explanation if needed.
Related to #123
Types: feat, fix, docs, refactor, test, chore
Examples:
feat: add message provider verification builder
Adds buildMessageVerifierOptions for message-based Pact verification.
Supports the same broker URL and selector logic as HTTP verification.
fix: handle missing authorization header case-insensitively
The request filter now checks for both 'Authorization' and 'authorization'
headers before adding a new one.
Available npm Scripts
| Script | Description |
|---|---|
npm run validate | Run typecheck, lint, test, and format in parallel |
npm run build | Clean and build CJS, ESM, and type declarations |
npm run clean | Remove the dist/ directory |
npm run test:lib | Run library unit tests with Vitest |
npm run test | Run all tests (library + sample app) |
npm run test:pact:consumer | Run sample app Pact consumer tests |
npm run test:pact:provider:local | Run sample app Pact provider tests locally (CDCT + MQ) |
npm run test:pact:provider:local:contract | Run local provider CDCT verification only |
npm run test:pact:provider:local:message | Run local provider message verification only |
npm run test:pact:local | Run consumer then provider Pact tests locally |
npm run test:pact:provider:remote | Run provider verification against remote broker (CDCT + MQ) |
npm run test:pact:provider:remote:contract | Run remote provider CDCT verification only |
npm run test:pact:provider:remote:message | Run remote provider message verification only |
npm run typecheck | TypeScript type checking without emit |
npm run lint | ESLint with auto-fix |
npm run fix:format | Prettier formatting |
npm run start:sample-app | Start sample app backend and frontend |
npm run publish:local | Publish the package locally via script |
npm run publish:pact | Publish Pact contracts to the broker |
npm run can:i:deploy:consumer | Check if consumer can be deployed |
npm run can:i:deploy:provider | Check if provider can be deployed |
npm run record:consumer:deployment | Record consumer deployment in broker |
npm run record:provider:deployment | Record provider deployment in broker |
npm run docs:dev | Start VitePress docs dev server |
npm run docs:build | Build VitePress documentation |
npm run docs:preview | Preview built documentation |
Release Process
Publishing via GitHub UI
- Go to Actions and select the "Publish Package" workflow.
- Click "Run workflow" and select the version type (patch, minor, major, or custom).
- Review and merge the generated PR.
Local Publishing
npm run publish:local
The package publishes to the public npm registry under the @seontechnologies scope.
Getting Help
- Issues: Report bugs and feature requests via GitHub Issues.
- Repository: github.com/seontechnologies/pactjs-utils
Thank you for contributing to pactjs-utils.