How to Implement API Contract Testing with Pact in Node.js (2026 Guide)
As of March 2026, microservices architecture remains the standard for building scalable applications. But with multiple services communicating via APIs, how do you ensure that changes in one service do not break your consumers? That is where contract testing comes in — and Pact is the leading tool for the job.
In this guide, you will learn what contract testing is, why it matters, and how to implement it in Node.js using Pact (v3.x as of early 2026).
What is Contract Testing?
Contract testing verifies that an API provider and its consumers agree on the API structure. Instead of spinning up both services, each consumer defines what it expects from the API, and the provider verifies it can satisfy those expectations.
Think of it like a legal contract: the consumer specifies what it needs (the "expectations"), and the provider signs off that it delivers exactly that.
Why Contract Testing Beats Integration Tests
| Aspect | Integration Tests | Contract Tests |
|---|---|---|
| Setup | Requires all services running | Tests run in isolation |
| Speed | Slow (full stack) | Fast (unit-level) |
| Failure isolation | One failure affects all | Pinpoints the broken side |
| CI/CD | Heavy resource usage | Lightweight |
Setting Up Pact in Node.js
Install the core packages:
npm install @pact-foundation/pact@^14.0.0 @pact-foundation/pact-core@^14.0.0
Project Structure
my-api/
├── provider/
│ ├── index.js
│ └── pact/
│ └── provider.test.js
└── consumer/
├── index.js
├── client.js
└── pact/
└── consumer.test.js
Writing Consumer Tests (The Expectation)
The consumer defines what it expects from the API. Let us say we have a user service that expects a list of users:
// consumer/pact/consumer.test.js
const { like, term } = require("@pact-foundation/pact-core");
const pact = require("@pact-foundation/pact").PactV4;
describe("User Consumer", () => {
const provider = pact({
consumer: "user-web-app",
provider: "user-api",
port: 4000,
log: path.resolve(process.cwd(), "logs", "pact.log"),
dir: path.resolve(process.cwd(), "pacts"),
logLevel: "INFO",
});
beforeAll(() => provider.setup());
afterAll(() => provider.finalize());
describe("fetching users", () => {
it("returns a list of users", async () => {
// Define the expected response structure
provider
.given("users exist")
.uponReceiving("a request for all users")
.withRequest({
method: "GET",
path: "/api/users",
headers: {
Accept: "application/json",
},
})
.willRespondWith({
status: 200,
headers: {
"Content-Type": "application/json",
},
body: like({
users: like([
{
id: like(1),
name: like("John Doe"),
email: like("john@example.com"),
},
]),
}),
});
await provider.executeTest(async (mockServer) => {
const response = await fetch(`${mockServer.url}/api/users`);
const data = await response.json();
expect(response.status).toBe(200);
expect(data.users).toHaveLength(1);
expect(data.users[0].name).toBe("John Doe");
});
});
});
});
Key Concepts in Consumer Tests
- Given: Sets up the provider state (e.g., "users exist")
- uponReceiving: Describes the consumer action
- withRequest: Specifies the HTTP request
- willRespondWith: Defines the expected response
- like(): Creates flexible matchers (ignores actual values, validates structure)
- term(): Creates matchers for specific patterns (regex)
Writing Provider Tests (The Verification)
The provider verifies it can satisfy all registered contracts:
// provider/pact/provider.test.js
const { VerifierV4 } = require("@pact-foundation/pact-core");
const path = require("path");
describe("User API Provider", () => {
const verifier = new VerifierV4({
provider: "user-api",
providerBaseUrl: "http://localhost:3000",
pactUrls: [
path.resolve(
process.cwd(),
"../consumer/pacts/user-web-app-user-api.json"
),
],
stateHandlers: {
"users exist": () => {
// Set up test data in your database
console.log("Setting up: users exist");
// Your database setup logic here
},
},
requestFilters: [
// Add auth headers if needed
(req, res, next) => {
req.headers["Authorization"] = "Bearer test-token";
next();
},
],
});
it("validates the expectations of user-web-app", async () => {
const result = await verifier.verify();
console.log("Contract verification result:", result);
});
});
Running Contract Tests in CI/CD
Consumer CI (Runs on Every Commit)
# .github/workflows/consumer-tests.yml
name: Consumer Contract Tests
on: [push]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: "22"
- run: npm ci
- run: npm test
env:
CI: true
- uses: actions/upload-artifact@v4
if: always()
with:
name: pact-contracts
path: pacts/
Provider CI (Runs Before Deployment)
# .github/workflows/provider-tests.yml
name: Provider Contract Tests
on:
push:
branches: [main]
pull_request:
jobs:
verify:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: "22"
- run: npm ci
- run: npm run start:provider &
- run: npm test:provider
env:
CI: true
Publishing Contracts to Pact Broker
For teams with multiple services, use a Pact Broker to share contracts:
const { Publisher } = require("@pact-foundation/pact-core");
async function publishContracts() {
const publisher = new Publisher({
pactFilesOrDirs: ["./pacts"],
pactBroker: "https://your-broker.pact.dius.com.au",
pactBrokerToken: process.env.PACT_BROKER_TOKEN,
consumerVersion: process.env.GITHUB_SHA?.substring(0, 7),
});
await publisher.publish();
}
publishContracts();
Best Practices for Contract Testing (2026)
- Start with consumer tests — They are easier to write and catch issues early
- Use semantic versioning — Tag contracts with version numbers
- Run provider tests before deployment — Prevent breaking changes from reaching production
- Keep contracts small — Test the critical paths, not every edge case
- Use flexible matchers — Prefer like() over exact values to reduce test flakiness
Common Pitfalls
- Testing too much detail (breaks easily when irrelevant fields change)
- Not running provider tests in CI
- Forgetting to publish contracts to the broker
- Hardcoding URLs instead of using environment variables
Conclusion
Contract testing with Pact is essential for maintaining reliable API integrations in 2026. It catches breaking changes early, reduces test flakiness, and enables independent service deployments. Start with consumer tests, add provider verification, and integrate both into your CI/CD pipeline.
For more advanced scenarios, explore Pact support for asynchronous messaging, XML APIs, and GraphQL contracts.
Top comments (0)