Versioning API contracts: managing breaking changes and testing compatibility
In an API’s first version, adding a field, renaming an endpoint or adjusting an error body can look like a small refactor. Once published, however, it becomes a dependency for another codebase. A web client, mobile app, browser extension, background worker or third-party integration may silently rely on the existing behaviour.
An API contract is more than an OpenAPI file or a DTO class. Its URL, HTTP method, request and response fields, defaults, ordering, pagination, error shape, authorisation behaviour and timeout expectations all belong to the contract. Sound versioning means knowing which change breaks consumers, which can safely extend the surface and how to prove the difference.
See consumers and usage patterns first
You cannot make a compatibility decision without knowing who uses an endpoint. Looking only at a frontend in the same repository is not enough. Older mobile releases, automation scripts and independent services can remain behind over time.
These questions help build an inventory:
- Which applications consume this endpoint or message?
- How often do clients update, and how old can active releases be?
- Is a field actually used, or does it exist only in documentation?
- If a consumer cannot be updated, what is the migration plan?
Access logs, API gateway metrics and code search can help. Low field usage does not mean it can be safely removed: a low-volume integration may be business-critical. Versioning decisions need technical observation and product ownership.
Make extension the default design
The safest compatible changes add optional information without changing existing meaning. Adding a response field is usually safe when clients are written to ignore unknown fields. Adding a new endpoint or a new error-code class with a distinguishable body also does not break existing calls.
There is no rule that adding a field is always safe, though. Clients with strict JSON schemas can treat unknown fields as errors. Adding a new option to an enum deserves particular care: adding archived to a status field can break a client that assumes only active and inactive. Consumers handling unknown values safely should be an explicit part of the contract.
When adding a request field, the server’s default behaviour must be clear. The answer to “what happens when the field is absent?” should not change between releases. A required new field is usually breaking; a temporary default, two-stage migration or new endpoint may be better.
Recognise breaking changes early
The following changes should be treated as breaking for most consumers:
- changing an endpoint or HTTP method,
- adding a required request field,
- removing, renaming or changing the type of a response field,
- changing default ordering or pagination meaning,
- moving a successful response to a different HTTP status,
- narrowing or otherwise changing authorisation or error behaviour.
Some changes are technically invisible but change business meaning. Moving a currency value from integer to decimal, switching a date from local time to UTC, or turning an empty array into null can affect consumers even when the schema looks similar. A contract review should examine units, time zone, empty-value meaning and ordering guarantees as well as field types.
Deprecation is a process
Instead of deleting an old field or endpoint, first mark it as deprecated. Documentation should show the replacement, a migration example and a target date. Where appropriate, return deprecation or warning information that prompts integration owners to act.
The process is more than an announcement. Usage metrics should show whether the old path was actually abandoned. Deciding from consumer migration and risk assessment is safer than relying only on a calendar date. For high-risk clients, supporting two contracts in parallel can cost less than an abrupt outage.
When a new major version is needed, options include /v2 in the URL, header-based versioning or media-type versioning. There is no universally correct mechanism; the important point is applying the choice consistently and being able to diagnose which version a client uses.
Make the contract a testable artefact
When documentation and the running application diverge, documentation quickly loses trust. Producing the contract as an artefact in source control and reviewing a diff on change is a first defence. An explicit schema makes removed or newly required fields visible.
Provider-side contract tests verify that real endpoints match the schema and example behaviour. On the consumer side, critical clients can publish their expectations as tests. A provider can then see which consumer is affected in CI before changing a field.
These tests should cover more than successful responses. Unauthorised access, validation failures, pagination boundaries, empty results, rate limits and retry behaviour are also part of the agreement. An API’s reliability comes as much from consistency on error paths as on the happy path.
Do not forget message contracts
Events published through a queue are APIs as well. Changing a field in an OrderCreated event carries the same compatibility responsibility as changing an HTTP endpoint. The event schema version, producer, creation time and idempotency information should be explicit.
Consumers should ignore unknown fields and treat events as potentially out of order or duplicated. Adding a new event type is often safer than overloading an existing event. If past events may need replaying, schema evolution and migration strategy should be considered from the beginning.
Good versioning does not slow change
Contract discipline can look like bureaucracy, but it makes the cost of change visible. Teams that know what consumers expect, what a change can break and how a migration is observed can move faster and with less stress.
A practical start is simple: source-controlled schemas for critical endpoints, compatibility diffs, a few contract tests and clear deprecation notes. Once these are established, an API stops being an implementation detail of one service and becomes a durable product surface that different teams can build on safely.