Contract Testing
Problem
Frontend and backend teams evolve interfaces independently, causing runtime failures when APIs change. Components expect fields that no longer exist, or type mismatches between what the API returns and what the frontend expects go undetected until production.
Solution
Verify that services honor their API contracts by testing request/response shapes rather than implementation details. This catches integration issues early when one service changes in ways that break consumers.
Example
This example demonstrates how to verify that an API response adheres to its expected contract by validating the shape and types of returned data.
// Test that API response matches expected contract
test('user API returns valid contract', async () => {
// Make actual API call to verify contract
const response = await fetch('/api/user/123');
const user = await response.json();
// Verify response has all required fields with correct types
expect(user).toMatchObject({
id: expect.any(Number), // ID must be a number
name: expect.any(String), // Name must be a string
email: expect.stringMatching(/^[^\s@]+@[^\s@]+\.[^\s@]+$/) // Email must be valid format
});
});
Benefits
- Catches breaking API changes before they reach production.
- Enables frontend and backend teams to develop independently.
- Validates that actual API responses match expected contracts.
- Prevents runtime errors from unexpected data shapes.
- Can be run in CI to verify contracts on every deployment.
Tradeoffs
- Requires maintaining contract definitions separate from implementation.
- Tests can become brittle if contracts are too specific.
- May not catch all integration issues, only contract violations.
- Requires coordination between teams on contract changes.
- Can slow down development if contracts are too rigid or frequently change.