API Contract Testing with Pact
Contract testing solves the #1 microservices integration problem: a service changes its API without telling the services that consume it, and everything breaks silently. Pact is the leading contract testing framework — it ensures that the API contract (the agreed-upon request/response format) is never broken without explicit negotiation between teams. This is the testing technique that enables safe, independent microservice deployments.
What is Contract Testing and Why It Matters
- Problem: 4 microservices consume the User API. User team changes a field from 'userId' to 'id'. Consumer services break. Nobody told anyone.
- Traditional solution: Integration testing in a shared staging environment — slow, fragile, requires all services deployed together
- Contract testing solution: Each consumer defines the contract (exactly what they need from the API). Pact verifies the provider fulfills all consumer contracts.
- Consumer-Driven Contracts: Consumers define what they expect; provider runs Pact verification to confirm it delivers what all consumers need
- Benefits: Catch breaking API changes before deployment; teams can work independently; fast (no shared environment needed); explicit contract negotiation
Pact Consumer and Provider Tests
// ══════════════════════════════════════════════════════════════
// PACT CONSUMER TEST (runs in Consumer service codebase)
// "I, Order Service, expect User API to return this:"
// npm install @pact-foundation/pact
// ══════════════════════════════════════════════════════════════
const { PactV3, MatchersV3 } = require("@pact-foundation/pact");
const { like, string, integer } = MatchersV3;
describe("Order Service → User API contract", () => {
const provider = new PactV3({
consumer: "OrderService",
provider: "UserAPI",
dir: "./pacts", // Contract file saved here
});
it("retrieves user for an order", async () => {
provider
.given("User with ID 123 exists")
.uponReceiving("a request for user 123")
.withRequest({
method: "GET",
path: "/api/users/123",
headers: { Authorization: like("Bearer some-token") },
})
.willRespondWith({
status: 200,
body: {
id: integer(123), // Must be integer
name: string("Alice"), // Must be a string
email: string("alice@example.com"), // Must be a string
// ⚠️ Order service only needs id, name, email — NOT all fields
// Pact only validates what the consumer declares it needs
},
});
// Run the actual consumer code against Pact's mock server
await provider.executeTest(async (mockServer) => {
const user = await getUser(mockServer.url, 123); // Your actual service code
expect(user.id).toBe(123);
expect(user.name).toBe("Alice");
});
// Pact saves: pacts/OrderService-UserAPI.json (the contract file)
});
});
// ══════════════════════════════════════════════════════════════
// PACT PROVIDER VERIFICATION (runs in Provider service codebase)
// "I, User API, verify I fulfill all consumer contracts"
// ══════════════════════════════════════════════════════════════
const { Verifier } = require("@pact-foundation/pact");
describe("User API — Pact Provider Verification", () => {
it("validates contracts from all consumers", () => {
return new Verifier({
provider: "UserAPI",
providerBaseUrl: "http://localhost:3001", // Your running API
// Load contracts from Pact Broker (central contract registry)
pactBrokerUrl: "https://your-pact-broker.io",
pactBrokerToken: process.env.PACT_BROKER_TOKEN,
// Or from local files:
// pactUrls: ["./pacts/OrderService-UserAPI.json"],
publishVerificationResult: true,
providerVersion: "2.1.0",
}).verifyProvider();
// FAILS if User API doesn't meet any consumer's declared requirements
// This prevents breaking changes from reaching production
});
});Common Mistakes
- Consumers declaring too much in contracts — only declare the fields your service actually uses; declaring everything makes contracts brittle to changes that don't affect you
- Not running provider verification in CI — contract verification must run on every provider deployment; otherwise breaking changes slip through
- Skipping Pact Broker — storing contracts as local files doesn't scale; use PactFlow or open-source Pact Broker for central contract management
- Treating Pact as integration testing — contract tests replace integration tests for API contracts, but they don't replace full E2E tests for user workflows
Tip
Tip
Practice API Contract Testing with Pact in small, isolated examples before integrating into larger projects. Breaking concepts into small experiments builds genuine understanding faster than reading alone.
Microservices safety net. Pact framework.
Practice Task
Note
Practice Task — (1) Write a working example of API Contract Testing with Pact from scratch without looking at notes. (2) Modify it to handle an edge case (empty input, null value, or error state). (3) Share your solution in the Priygop community for feedback.
Quick Quiz
Common Mistake
Warning
A common mistake with API Contract Testing with Pact is skipping edge case testing — empty inputs, null values, and unexpected data types. Always validate boundary conditions to write robust, production-ready software testing code.
Key Takeaways
- Contract testing solves the #1 microservices integration problem: a service changes its API without telling the services that consume it, and everything breaks silently.
- Problem: 4 microservices consume the User API. User team changes a field from 'userId' to 'id'. Consumer services break. Nobody told anyone.
- Traditional solution: Integration testing in a shared staging environment — slow, fragile, requires all services deployed together
- Contract testing solution: Each consumer defines the contract (exactly what they need from the API). Pact verifies the provider fulfills all consumer contracts.