Contract Tests and API Tests: The Public System Agreement
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:
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:
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:
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:
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:
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:
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.