level 05 / contract-and-schema
Contract Testing & Schema Validation
Validate API response shapes with JSON Schema and understand consumer-driven contract testing.
What Is Contract Testing?
A contract is the agreed-upon interface between a consumer (frontend, mobile app) and a provider (API). Contract testing verifies both sides honour it — before integration, in CI.
Consumer Provider
──────── ────────
Frontend app ←──────→ REST API
contract
(shape of
requests &
responses)
Two levels:
- Schema validation — assert response matches expected structure (fields, types)
- Consumer-driven contract testing (CDCT) — consumer publishes expectations; provider verifies them (Pact, PactFlow)
JSON Schema Validation with Ajv
import Ajv from 'ajv';
import addFormats from 'ajv-formats';
import { test, expect } from '@playwright/test';
const ajv = new Ajv();
addFormats(ajv);
const bookingSchema = {
type: 'object',
required: ['firstname', 'lastname', 'totalprice', 'depositpaid', 'bookingdates'],
properties: {
firstname: { type: 'string' },
lastname: { type: 'string' },
totalprice: { type: 'number' },
depositpaid: { type: 'boolean' },
bookingdates: {
type: 'object',
required: ['checkin', 'checkout'],
properties: {
checkin: { type: 'string', format: 'date' },
checkout: { type: 'string', format: 'date' },
},
},
},
};
test('booking response matches schema', async ({ request }) => {
const res = await request.get('https://restful-booker.herokuapp.com/booking/1');
expect(res.status()).toBe(200);
const body = await res.json();
const valid = ajv.validate(bookingSchema, body);
if (!valid) throw new Error(ajv.errorsText());
});
TypeScript Interfaces as Living Contracts
Use TypeScript to enforce shape at compile time:
interface Booking {
firstname: string;
lastname: string;
totalprice: number;
depositpaid: boolean;
bookingdates: { checkin: string; checkout: string };
additionalneeds?: string;
}
test('cast and assert booking shape', async ({ request }) => {
const res = await request.get('/booking/1');
const body = (await res.json()) as Booking;
expect(typeof body.firstname).toBe('string');
expect(typeof body.totalprice).toBe('number');
});
Consumer-Driven Contract Testing (CDCT)
Problem: Integration tests catch contract breaks late (deployed environment). CDCT catches them in CI, per service.
Flow with Pact:
- Consumer writes a test describing what it expects from the provider
- Pact generates a contract file (
*.json) - Provider verifies its implementation against the contract
- Pact Broker stores and versions contracts
// consumer pact test (simplified)
await provider.addInteraction({
state: 'booking exists',
uponReceiving: 'a request for booking 1',
withRequest: { method: 'GET', path: '/booking/1' },
willRespondWith: {
status: 200,
body: like({ firstname: 'Jane', totalprice: 150 }),
},
});
Schema Drift Detection
Run schema validation in CI against production snapshots:
// record snapshot
const snapshot = await (await request.get('/api/schema')).json();
fs.writeFileSync('schema-snapshot.json', JSON.stringify(snapshot));
// compare in next CI run
const current = await (await request.get('/api/schema')).json();
const previous = JSON.parse(fs.readFileSync('schema-snapshot.json', 'utf8'));
expect(current).toMatchObject(previous); // new fields ok; removed fields fail