Contract Testing Concepts
How teams keep an API's consumers and provider in sync without one giant end-to-end test suite for every possible combination.
What you'll learn
- Explain what a contract is in the context of API testing
- Distinguish contract testing from full end-to-end integration testing
- Identify a contract-breaking change to an API response
Prerequisites
Explanation
A frontend team and a backend team both work on the same product, but on separate schedules. The frontend expects an API response shaped a certain way; the backend, refactoring internally, changes a field name without realizing three different consumers depend on the old one. Nobody notices until the frontend breaks in production. Contract testing exists to catch exactly this class of problem early, without requiring the frontend and backend to be deployed together and fully end-to-end tested on every single change.
A contract is an explicit, agreed-upon description of what a consumer expects from a provider: which endpoints exist, what request shape they accept, and what response shape they return. Once written down, the contract becomes something both sides can test against independently: the provider verifies its actual API still satisfies the contract every time it changes; the consumer verifies its code correctly handles data shaped according to the contract, without needing a live version of the real backend running at all.
This is meaningfully different from full end-to-end integration testing, which spins up real, connected versions of both systems and exercises them together — thorough, but slow, and it only tests the specific combination of versions that happen to be running at that moment. Contract testing is faster (each side tests independently against a shared, explicit specification) and catches breaking changes earlier, often before the two services are ever deployed together at all.
Common contract-breaking changes worth specifically watching for: renaming a field the consumer reads, changing a field's type (a number becoming a string), removing a field a consumer depended on, or making a previously-optional field required in the request. Not every response change breaks a contract — adding a new, additional field that no existing consumer reads yet is usually safe (this is why consumers are conventionally expected to ignore fields they don't recognize, rather than rejecting anything unfamiliar).
Example
A tiny contract check: does an actual (simulated) response still satisfy what a consumer explicitly depends on?
const contract = { requiredFields: ["id", "email"], fieldTypes: { id: "number", email: "string" } };
// Simulated provider response -- not a real network call.
const actualResponse = { id: 42, email: "ada@example.com", newInternalFlag: true };
function satisfiesContract(response, contract) {
return contract.requiredFields.every(
(field) => field in response && typeof response[field] === contract.fieldTypes[field],
);
}
console.log(satisfiesContract(actualResponse, contract)); // true -- the extra field is fine, ignoredTry it yourself
Rename 'email' to 'emailAddress' in actualResponse (a breaking change) and re-run.
Code editor. Press Escape then Tab to leave the editor if keyboard focus becomes trapped. Press Control+Shift+M inside the editor to toggle Tab-key focus trapping.
Guided exercise
Guided exercise
A consumer's contract requires a 'total' field of type number. Two provider changes are proposed. Set isBreakingA and isBreakingB: changeA renames 'total' to 'totalAmount'. changeB adds a brand-new optional 'currency' field alongside the existing 'total' field.
Checks: correctly identifies the rename as breaking · correctly identifies the additive change as non-breaking
Code editor. Press Escape then Tab to leave the editor if keyboard focus becomes trapped. Press Control+Shift+M inside the editor to toggle Tab-key focus trapping.
Stuck? Get a hint.
Independent exercise
Independent exercise
Write a function isBreakingChange(oldContract, newResponse) that returns true if any field listed in oldContract.requiredFields is either missing from newResponse or has a different typeof than oldContract.fieldTypes says.
Checks: a conforming response is not flagged · a type change is flagged as breaking · a missing field is flagged as breaking · an extra field is not flagged as breaking
Code editor. Press Escape then Tab to leave the editor if keyboard focus becomes trapped. Press Control+Shift+M inside the editor to toggle Tab-key focus trapping.
Stuck? Get a hint.
Common mistakes
- Relying only on full end-to-end tests to catch breaking API changes, which is slower and only tests the exact version combination currently deployed.
- Treating every response change as equally risky, when adding a new unused field is usually safe and removing or retyping an existing one usually isn't.
- Writing a contract once and never updating it as the consumer's real needs change, letting it drift from reality.
Knowledge check
Takeaway
Contract testing lets consumers and providers verify compatibility independently against an explicit, shared specification, catching breaking changes earlier and faster than full end-to-end testing alone.
Summary
This lesson introduced contract testing as a way to catch API compatibility breaks early, and distinguished breaking changes (renames, removals, type changes) from safe additive changes.
References
Your notes
Notes save automatically.
Finished this lesson?
Mark it complete to track your progress and schedule a future review.
AI tutor
The optional AI tutor isn't enabled in this deployment. All lessons, exercises, quizzes, and search work fully without it.