API & Contract Testing
Verifying that services agree on their API without deploying them together — consumer-driven contracts (Pact), provider verification in CI, and gating deploys on compatibility.
Contract testing verifies the agreement between two services — what requests a consumer makes and what responses it depends on — without running both together. With consumer-driven contracts (Pact), each consumer publishes a contract of its needs, and the provider's CI verifies it still complies. This catches breaking API changes early and lets services deploy independently with confidence.
When services talk to each other, you need confidence they still agree on the API. Full end-to-end tests that deploy everything together are slow, flaky, and hard to run on every change. Contract testing verifies the agreement between two services in isolation — fast and reliable.
The consumer expresses exactly what it needs from the provider (specific endpoints, fields, and shapes) as a contract — a 'pact'. This captures real usage: only the parts the consumer actually relies on are pinned down, so the provider knows precisely what it must not break.
The pact is published (e.g. to a Pact Broker), and the provider's CI replays it against the real provider, asserting every expectation still holds. If the provider drops or renames a field a consumer depends on, verification fails in the provider's pipeline — before the change ever ships.
Because each side is tested against the shared contract separately, services can be developed and deployed independently with confidence. Tools like 'can-i-deploy' gate a release on contract compatibility across all consumers, catching breaking API changes without spinning up the whole system.
Key Concepts
A machine-readable agreement of the requests and responses a consumer relies on from a provider.
Consumers define what they need; the contract reflects real usage, not the provider's full API.
Replaying the consumer's pact against the real provider in CI to confirm it still complies.
A shared registry that stores pacts and verification results between consumer and provider pipelines.
A gate that blocks a release unless it's contract-compatible with everything already in the target env.
A lighter relative — validating responses against a shared schema (e.g. OpenAPI) rather than per-consumer pacts.
1// CONSUMER side — declare what you depend on; generates a pact file.2provider.given("user 1 exists")3 .uponReceiving("a request for user 1")4 .withRequest({ method: "GET", path: "/users/1" })5 .willRespondWith({6 status: 200,7 body: { id: like(1), name: like("Ada"), email: like("ada@x.com") },8 }); // → users-consumer.pact.json published to a broker910// PROVIDER side — verify the REAL provider still satisfies every pact.11await new Verifier().verifyProvider({12 provider: "user-service",13 pactBrokerUrl: process.env.PACT_BROKER_URL, // fetch all consumer pacts14 providerBaseUrl: "http://localhost:5000", // run them against the real API15}); // ❌ fails in CI if a field a consumer needs is gone1617// 'can-i-deploy' gates a release on contract compatibility:18// pact-broker can-i-deploy --pacticipant user-service --version $SHA --to prod
In microservices, breaking API changes between services are a top cause of integration failures, and full E2E suites are too slow and flaky to catch them reliably. Contract testing is the standard, efficient answer — making it valuable knowledge for backend and platform roles and a recurring topic in microservices design discussions.
Common Pitfalls
1The Field Rename That Broke Three Apps
A backend team renamed a JSON field (email → contact) in the user-service, considering it a minor change. It silently broke a web app, a mobile app, and a reporting job that all read user.email — discovered only after deploy.
There was no automated check that consumers still got what they depend on. The provider had no visibility into how its API was actually used, so a 'small' change shipped as a breaking one.
Introduce consumer-driven contract tests: each consumer publishes a pact of the fields it needs. The provider's CI verifies all pacts on every change, so renaming a field a consumer relies on fails the build immediately — and can-i-deploy blocks the release until it's compatible.
Takeaway: Contract tests make hidden consumer dependencies explicit and enforce them in CI. Breaking API changes are caught in the provider's pipeline, not in production after the fact.
2Replacing a Slow, Flaky E2E Gate
A team gated every deploy on a giant end-to-end suite that spun up all services. It took 40 minutes, was frequently flaky, and still missed contract mismatches because it only exercised a few happy paths.
Heavyweight E2E tests are slow, brittle, and expensive to maintain, yet provide weak guarantees about service-to-service compatibility. They became a bottleneck that eroded trust without truly preventing integration breakage.
Move API-compatibility checks to fast, deterministic contract tests run independently on each side, keeping only a thin layer of true end-to-end smoke tests. Compatibility is now verified in minutes per service, and breaking changes are caught precisely where they're introduced.
Takeaway: Contract tests give most of the integration confidence of E2E tests at a fraction of the cost and flakiness. Verify agreements per service-pair, and reserve full E2E for a few critical smoke paths.
Join the discussion
Comments
Loading comments...
Cookie preferences
We use essential cookies for sign-in and preferences. With your permission, we also use basic analytics to improve lessons.