Use whenever two services (a frontend and an API, or two microservices) keep breaking each other's API: set up Pact consumer-driven contract tests, publish pacts to a broker, verify the provider and gate deploys with can-i-deploy.
91%
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
Passed
No findings from the security scan
The canonical home for this skill is testland/pact-contract-testing
Pact is a code-first tool for consumer-driven contract tests over HTTP and message integrations (pact-overview). The contract is a side-effect of the consumer's tests - each test documents one request/response pair, and only the parts the consumer actually uses get tested.
The lifecycle has five steps (pact-how-it-works):
given() → uponReceiving() → withRequest() → willRespondWith() interaction.can-i-deploy queries the Broker matrix to confirm the candidate version
has a green verification against every consumer/provider already deployed in
the target environment (can-i-deploy).@pact-foundation/pact, pact-jvm-consumer,
pact-python, etc., or a .pact/ directory is present.If the API has no consumers under your control (public or third-party APIs),
prefer openapi-contract-diff - schema diffs need no consumer coordination.
npm install --save-dev @pact-foundation/pact(Per pact-js.)
PactV3const { PactV3, MatchersV3 } = require('@pact-foundation/pact');
const { like, eachLike } = MatchersV3;
const provider = new PactV3({
consumer: 'web-app',
provider: 'pet-service',
dir: path.resolve(process.cwd(), 'pacts'),
});
describe('Pet Service consumer', () => {
it('returns a list of dogs', async () => {
provider
.given('I have a list of dogs')
.uponReceiving('a request for all dogs with the builder pattern')
.withRequest({
method: 'GET',
path: '/dogs',
headers: { Accept: 'application/json' },
})
.willRespondWith({
status: 200,
headers: { 'Content-Type': 'application/json' },
body: eachLike({
id: like(1),
name: like('Rex'),
}),
});
await provider.executeTest(async (mockServer) => {
const response = await fetch(`${mockServer.url}/dogs`);
expect(response.status).toBe(200);
});
});
});(Adapted from pact-js.)
given() describes a provider state the verifier sets up later. Matchers
like like() and eachLike() assert type/shape rather than exact values, so
the provider isn't bound to fixture-specific data.
By default, PactV3 writes pact files to ./pacts/ relative to the
process CWD (pact-js). Each consumer/provider pair produces one
JSON file: <consumer>-<provider>.json.
The Broker is the central registry for pact files and verification results. Publishing happens after the consumer tests pass:
npx pact-broker publish ./pacts \
--consumer-app-version=$(git rev-parse HEAD) \
--branch=$(git rev-parse --abbrev-ref HEAD) \
--broker-base-url=$PACT_BROKER_BASE_URL \
--broker-token=$PACT_BROKER_TOKENThe Broker stores (overview):
production, staging, main).Tagging by branch + the Git SHA as consumer-app-version is the
canonical pattern - can-i-deploy queries the matrix using these
identifiers.
const { Verifier } = require('@pact-foundation/pact');
new Verifier({
providerBaseUrl: 'http://localhost:8081',
pactBrokerUrl: process.env.PACT_BROKER_BASE_URL,
pactBrokerToken: process.env.PACT_BROKER_TOKEN,
provider: 'pet-service',
providerVersion: process.env.GIT_SHA,
providerVersionBranch: process.env.GIT_BRANCH,
publishVerificationResult: true,
consumerVersionSelectors: [
{ mainBranch: true },
{ deployedOrReleased: true },
],
}).verifyProvider();(Adapted from pact-js.)
consumerVersionSelectors controls which consumer pacts get verified -
mainBranch plus deployedOrReleased keeps the provider compatible with both
the latest consumer work and what's in production (pact-how-it-works).
publishVerificationResult: true is what updates the matrix - without it the
broker has no record of this provider version's verification status.
For each consumer interaction with a given(<state>), the provider
test setup must register a hook that puts the system into that state
before replay. Using Express:
new Verifier({
...
stateHandlers: {
'I have a list of dogs': async () => {
await db.dogs.bulkInsert([{ id: 1, name: 'Rex' }]);
return { description: 'dogs seeded' };
},
},
}).verifyProvider();State setup that fails causes the matching interaction to fail verification.
can-i-deploy - deployment gatePer can-i-deploy:
pact-broker can-i-deploy \
--pacticipant pet-service \
--version $(git rev-parse HEAD) \
--to-environment production| Flag | Required | Effect |
|---|---|---|
--pacticipant | yes | Application (consumer or provider) name. |
--version | yes | App version string (typically the Git SHA). |
--to-environment | optional | Target environment to check against. Omit to check against latest. |
Exit codes (can-i-deploy):
0 - "Computer says yes \o/" - safe to deploy.1 - "Computer says no" - at least one matrix cell is missing or
failed.The output includes a markdown-style matrix of consumer/provider version pairs with verification status and links to detailed verification results.
After deploying, record the deployment so subsequent can-i-deploy
queries see the new "currently deployed" baseline:
pact-broker record-deployment \
--pacticipant pet-service \
--version $(git rev-parse HEAD) \
--environment productionSkipping this step is the most common reason can-i-deploy returns
spurious "no" verdicts.
Wire publish -> verify -> can-i-deploy into the pipeline for both sides. Full
consumer and provider GitHub Actions steps are in
references/ci-pipelines.md.
can-i-deploy is the actual gate - pact verification happens in step 1, but a
green verification alone doesn't prove every paired version is compatible.
PactV3 DSL + Verifier setup.testland/qa
aa16752
Canonical home
since Jul 23, 2026
If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.