What contract testing is
The agreement between an API and its clients, the bugs that break it, and how contract tests differ from the rest.
An API is useful only because other programs rely on it. A web shop's frontend expects GET /api/Products/1 to return an object with a pricethat is a number. A mobile app expects a 404 for a product that doesn't exist, with a message it can show. Those expectations, taken together, are a contract between the API and the programs that use it. Contract testing checks that both sides still keep it. This lesson explains what that means and why ordinary tests miss the bugs contract tests catch.
Running the examples
The tutorial's code is plain JavaScript with no libraries, so every line is readable, including the validator lesson 3 builds. You need Node.js 22 or later. Download the project, unzip it and run:
$ node --version
v24.14.1
$ cd contract-testing
$ npm test
...
ℹ tests 44
ℹ pass 44
ℹ fail 0The tests read the API's published OpenAPI document and call the API, and only one of them changes anything: it creates a todo and deletes it again.
Provider and consumer
Contract testing has its own two words for the two sides:
- The provider is the API: here, api.testingapis.com.
- A consumeris any program that calls it: a web frontend, a mobile app, another backend service, a partner's integration, or your own test suite.
A contract covers everything a consumer could reasonably depend on:
| Part of the contract | Example |
|---|---|
| Which endpoints exist, with which methods | GET /api/Products/{id} |
| Parameters and their types | id is an integer, in the path |
| Request bodies: fields, types, which are required | Creating a product needs a title |
| Status codes | 200 for a product, 404 when there isn't one |
| Response fields, their types, and whether they're always present | price is a number, always there |
| Error bodies | { status, error, message } |
| Content types | application/json |
Why ordinary tests miss contract breaks
Here is how a contract usually breaks:
Monday The backend team renames a field in GET /api/Products/{id}: inStock → available
Their tests pass: they updated them along with the code.
Tuesday The release goes out.
Tuesday The shop's product page reads product.inStock, gets undefined,
and shows every product as sold out.
Wednesday Someone notices the sales graph.Nobody did anything careless. The backend's tests were changed along with the code, which is what good tests are supposed to do, and so they passed. The frontend's tests use a fake API that still returns inStock, so they passed too. Each side was tested against its own idea of the API, and the two ideas had quietly separated. The bug lived in the gap between them.
A contract test closes that gap by checking both sides against one shared description of the API. If the backend renames inStock, a contract test fails before the release, because the contract still says inStock. The team then decides deliberately: keep the old name, add the new one alongside it, or agree a change with every consumer.
How contract tests differ from other API tests
| Test | Asks | Example |
|---|---|---|
| Functional | Does the API do the right thing? | Product 1 is the headphones and costs 249.99 |
| Contract | Does the API still have the promised shape? | Every product has a numeric price, and nothing unexpected |
| End to end | Does the whole system work together? | A user can find a product and buy it |
Contract tests check shapes, not values. They don't care that product 1 costs 249.99, only that priceis there and is a number. That makes them fast, and stable when the data changes, but it also means they don't replace functional tests. A price that is a number and wrong passes every contract test.
Two ways to write the contract down
- Provider-driven. The provider publishes a description of its whole API, usually an OpenAPI document, and that file is the contract. Tests check the real API against the file. Lessons 2 to 6 work this way.
- Consumer-driven. Each consumer writes down the requests it makes and the fields it actually uses, and the provider checks it still meets them all. A change no consumer depends on is free to make. Lesson 7 covers this.
Most teams that do contract testing well use both, and lesson 8 runs them in CI.
A contract that said almost nothing
A contract only protects what it describes. When work on this tutorial began, this API's OpenAPI document listed every endpoint, but here is how many of them it said anything about the response for:
$ node demos/spec-coverage.mjs # the spec as it was when this tutorial began
spec: https://api.testingapis.com/openapi/v1.json (OpenAPI 3.0.4)
operations: 400
described: 1 have a schema for their success response
GET /WeatherForecastOne out of 400, and that one was a leftover from a project template. The schemas existed, but no response pointed to them, because the code returned results the documentation generator couldn't see into. A consumer could rely on the spec for which endpoints existed and nothing about what came back. So the first contract test this tutorial ran found its first bug: the contract itself was too weak to test against. The API was fixed for the products, posts and todos resources, which the rest of the tutorial uses:
$ node demos/spec-coverage.mjs # after the fix
spec: https://api.testingapis.com/openapi/v1.json (OpenAPI 3.0.4)
operations: 400
described: 20 have a schema for their success response
GET /api/Products
POST /api/Products
DELETE /api/Products/{id}
GET /api/Products/{id}
PUT /api/Products/{id}
... and 15 moreCheck your understanding
1.In contract testing, which side is the provider and which is the consumer?Show answer
The provider is the API. A consumer is any program that calls it, including a test suite.
2.Both teams' tests passed, yet the release broke the product page. How?Show answer
Each side was tested against its own idea of the API: the backend updated its tests with the rename, and the frontend tested against a fake API with the old field. Nothing checked the two against a shared contract.
3.A product's price comes back as 2.49 instead of 249.99. Does a contract test catch it?Show answer
No. The shape is still right: price is a number. That's a job for a functional test.
4.What's the difference between provider-driven and consumer-driven contracts?Show answer
In provider-driven testing, the provider's published spec is the contract. In consumer-driven testing, each consumer states what it uses, and the provider checks it meets all of them.
Exercise
Pick an API you use at work or in a project, and list five things your code depends on that a change could break: a field name, a type, a status code, an error format. For each, find out whether the API's documentation promises it, or whether you're relying on behavior nobody wrote down. Then run node demos/spec-coverage.mjs in the example project with BASE_URLpointing at another API's server, if it publishes its spec at /openapi/v1.jsonsign in to see.