Konrad Kowalski (rootsher)Principal Platform & Reliability Architect111000000110111100011101000000111011101000001011

Contract Tests and API Tests: The Public System Agreement

date
category
Testing
also in
Backend
reading
3 min / 532 words

Changing a field in an API response often looks like a refactor.

The code is cleaner.

Unit tests pass.

The backend returns a new data shape.

The problem appears only when the frontend, worker, or older mobile app still expects the old agreement.

That is where contract tests and API tests belong.

What kind of test this is

API test checks the behavior of a public interface.

It can cover:

text
REST endpoint
GraphQL query
RPC method
webhook
event consumer
public SDK

Contract test checks the agreement between a producer and a consumer.

The agreement says:

text
what may be sent
what may be received
which fields are required
which fields are optional
what errors look like
what counts as a compatible change

An API test can be one-sided: the system exposes an endpoint and we check its response.

A contract test is relational: there is a producer and a consumer, and the test keeps their expectations aligned.

Schema tests

Schema test lives close to the same layer.

It checks whether data matches a schema:

text
OpenAPI
JSON Schema
GraphQL schema
AsyncAPI
Avro
Protobuf

A schema test does not prove business behavior is correct.

It proves the data shape fits the agreed structure.

That matters because many integration failures do not start with an algorithm.

They start with a field that disappeared, changed type, or became required.

What happens when they are missing

Without these tests, the public agreement starts depending on team memory.

Someone changes customerId to accountId.

Someone removes a status that "is not used anymore".

Someone changes error 409 to 400, because it is easier in the controller.

Each of these changes can make sense locally.

For a consumer, it can be a breaking change.

If E2E catches it, the problem is already hidden behind the whole user path.

If production catches it, we have a compatibility incident.

Consumer-driven contracts

In systems with many consumers, a good contract often starts with the consumer.

The consumer says:

text
I need these fields
for this request I expect this response
these are the errors I handle

The producer runs those expectations on its side.

This does not replace producer tests.

It adds missing information: which parts of the API are actually used and how.

Without that, the backend can maintain a formal schema that looks good but does not protect real clients.

Compatibility boundary

The most important question in contract tests is:

text
is this change compatible with existing consumers?

Adding an optional field is usually compatible.

Removing a field usually is not.

Changing a type usually is not.

Adding a new required field to a request is almost always a breaking change.

Changing semantics without changing schema is the hardest case, because a schema test will not see it.

Then an API test describing behavior is needed, not only shape.

Tool from the stack

For API tests in a Node backend I would choose Supertest.

It checks real endpoints, status codes, headers, and bodies without starting the full environment.

For contract tests I would add validation against OpenAPI or JSON Schema.

In practice the test should say:

text
the endpoint returns the behavior I expect
the response fits the public contract

If the API has many consumers, a schema is not enough.

Then consumer-driven contracts are worth adding, but I would still keep them close to the producer pipeline.

When this is not enough

A contract test and an API test still do not say the user can complete the whole path.

They can confirm that an endpoint works and the contract matches.

They will not confirm that the browser, routing, UI state, payment, and return from the payment gateway compose into one flow.

That needs E2E tests.