API Contract Testing Explained
A service passes every integration test, ships, and the next morning a mobile client crashes because a field quietly changed from a number to a string. Nobody broke a test — because no test checked the shape the client actually depends on. That's the gap contract testing closes: it verifies the interface between a provider and its consumers, independently of whether the full system works end to end.
What a contract is
A contract is a precise, machine-checkable description of a request/response pair: which fields exist, their types, which are required, and sometimes allowed values or formats. It is narrower than "does the feature work" and broader than "does this one example match" — a contract test fails when the shape changes, not when a value a client doesn't care about changes.
Contracts usually come from one of two places:
- Schema-first — an OpenAPI spec or JSON Schema is the source of truth, written before or alongside the implementation.
- Consumer-driven — each consumer records the exact fields it uses, and the provider is tested against the union of all consumer expectations (the pattern tools like Pact popularized).
Contract testing vs. integration testing
| Integration / E2E test | Contract test |
|---|---|
| Needs a running provider and its dependencies | Runs against a schema, a recorded sample, or a mock built from the contract |
| Verifies business behavior ("the order total is correct") | Verifies interface shape ("the response has a numeric total") |
| Slow, flaky when run across teams | Fast, runs in isolation per side |
| Catches logic bugs | Catches breaking changes before they reach consumers |
They're complementary, not competing: contract tests catch "this will break someone downstream" early and cheaply; integration tests still confirm the business logic is correct. See our broader REST API testing checklist for where each type fits.
A practical workflow: schema-first contracts
Most teams don't need a dedicated contract-testing framework to get real value — a JSON Schema per endpoint, checked on every response, already catches the majority of breaking changes.
- Derive the schema. Generate it from your OpenAPI spec, or write it by hand for endpoints without one yet. Keep it in the repo next to the code that implements the endpoint.
- Validate every response against it in your test requests. In RestRuno, a post-response script can run a schema check directly:
const schema = {
type: 'object',
required: ['id', 'email', 'createdAt'],
properties: {
id: { type: 'number' },
email: { type: 'string', format: 'email' },
createdAt: { type: 'string', format: 'date-time' },
plan: { type: 'string', enum: ['free', 'pro', 'enterprise'] }
}
};
rr.test('response matches the user contract', () =>
rr.expect(rr.response.body).toMatchSchema(schema)
);
- Run it on every pull request, not just before a release — a contract break caught in CI costs minutes; the same break caught by a consumer in production costs hours. See our guide on automating API tests in CI/CD.
- Version the contract explicitly when a change is genuinely breaking, instead of mutating it silently — see API versioning as the contract's release process.
Consumer-driven contracts, when schema-first isn't enough
A shared schema assumes every consumer needs every field it describes. In practice, a mobile app might only read three fields of a twenty-field response, while an internal dashboard reads all twenty. Consumer-driven contract testing flips the direction: each consumer publishes the exact subset of fields and behaviors it relies on, and the provider's CI pipeline replays those expectations against the real implementation before merging. This catches the realistic failure mode — "the field nobody on the provider team thought still mattered" — without forcing every consumer to agree on one giant shared schema.
This pattern pays off once you have more than two or three independent consumer teams; below that, a well-maintained OpenAPI spec plus schema validation usually covers the same ground with far less process.
Where mocks fit in
A contract is also the cleanest input for a mock server: generate one directly from the OpenAPI spec or JSON Schema, and consumer teams can build against a provider that doesn't exist yet, or one that's down. See mocking and stubbing APIs for reliable tests for how to keep that mock honest as the real contract evolves.
Common pitfalls
- Treating examples as contracts. A single recorded JSON response is not a schema — it can't express "this field is optional" or "this enum has three allowed values."
- Letting the contract drift from the code. If the OpenAPI spec isn't generated from (or validated against) the implementation, it rots within a few sprints.
- Over-specifying. Pinning fields a consumer doesn't use turns every harmless addition into a false failure. Validate what matters, not everything that's present.
- Skipping nullability and formats. Most real breaks are a field going from required to optional, or a date format changing — add those checks explicitly, don't assume type alone covers them.
Quick checklist
- ✔ Each endpoint has a schema (OpenAPI/JSON Schema), kept in the repo next to its code
- ✔ Responses are validated against that schema in automated tests, not just spot-checked by eye
- ✔ Contract checks run on every pull request, before merge, not only before release
- ✔ Breaking changes are versioned explicitly, never shipped silently
- ✔ Consumer-driven contracts considered once more than a couple of independent teams depend on the API
- ✔ Mock servers are generated from the same contract the tests validate against
Validate contracts without extra infrastructure
RestRuno runs JSON Schema assertions as part of normal post-response tests, in a free local-first client — no server to deploy, no account to create.
Download RestRuno — free