API Versioning and Compatibility: Evolving Contracts Without Surprise
📑 On this page
- A concrete example: changing identifiers
- Compatibility has several dimensions
- Additive changes are often safer
- Tolerant readers
- Version only when needed
- URL versioning
- Header and media-type versioning
- Date-based versions
- Field and endpoint deprecation
- Usage telemetry
- Contract testing
- Database changes and APIs
- Behavior changes
- Error compatibility
- Pagination compatibility
- Supporting multiple versions
- Client migration
- Compatibility and security
- Knowledge check
- The one idea to remember
An API provider can deploy new code in minutes while some clients remain unchanged for years.
This is especially true for mobile apps, partner integrations, devices, and scripts outside the provider's control.
Compatibility preserves behaviors clients depend on; versioning creates a deliberate boundary when incompatible behavior must change.
An API change is judged by client impact, not by how small the provider's code diff appears.
A concrete example: changing identifiers
Version 1 returns:
{ "id": 42 }The provider wants:
{ "id": "ord_01J8..." }Changing number to string can break:
- Static types
- Database columns
- Sorting
- URL construction
- Validation
- Cached data
This is not a small change because the identifier travels through many client assumptions.
A migration may add a new field first, support both, and version the final removal.
Compatibility has several dimensions
Clients can depend on:
- Field names
- Types
- Required fields
- Enum values
- Error codes
- Status codes
- Ordering
- Pagination
- Timing
- Consistency
- Retry behavior
- Authorization
- Side effects
Returning the same JSON shape while changing default ordering can break cursor traversal or user experience.
Compatibility includes semantics and operations, not only syntax.
Additive changes are often safer
Usually safer:
- Add optional response field
- Add new endpoint
- Add optional request field with unchanged default
- Add new capability
Potentially unsafe:
- Add required request field
- Remove or rename field
- Change type
- Change nullability
- Tighten validation
- Change meaning
Even adding an enum value can break clients with exhaustive switches that assume the original set is complete.
Providers should document extensible fields; clients should handle unknown values safely.
Tolerant readers
A tolerant client:
- Ignores unknown response fields
- Handles optional values
- Does not depend on object field order
- Uses documented identifiers
- Handles unknown enum values where appropriate
Tolerance helps providers add information.
It should not mean accepting malformed or security-sensitive input blindly.
Servers still validate requests strictly enough to preserve invariants.
Compatibility is a negotiated discipline on both sides.
Version only when needed
Creating a new API version for every additive field causes fragmentation.
Prefer compatible evolution within a version.
Version when:
- Meaning changes
- Old contract blocks necessary design
- Security requires incompatible behavior
- Data model changes fundamentally
- Errors or workflow cannot coexist cleanly
The goal is not avoiding all versions. It is making each version boundary meaningful and supportable.
URL versioning
Example:
/v1/orders
/v2/ordersBenefits:
- Visible
- Easy to route
- Easy to document and test
Costs:
- Versions can duplicate large API surfaces
- Resource identity may appear tied to version
- Clients may remain on old versions indefinitely
URL versions are pragmatic and widely understood.
The version usually represents the contract, not the server implementation.
Header and media-type versioning
A version can appear in a header:
Accept: application/vnd.example.orders.v2+jsonor custom version header.
Benefits:
- Stable resource URL
- Content negotiation
Costs:
- Less visible in browsers and logs
- Tooling and caching require care
- Harder for casual debugging
No placement removes migration work.
Choose one consistent scheme that fits clients and infrastructure.
Date-based versions
Some APIs assign behavior by a date:
API-Version: 2026-07-16This can bundle changes and let accounts upgrade deliberately.
It requires:
- Clear changelog
- Reproducible historical behavior
- Support policy
- Testing across dates
Do not imply every daily date is a unique implementation.
The date identifies a contract snapshot or release boundary.
Field and endpoint deprecation
Deprecation is a process:
- Announce replacement.
- Document migration.
- Mark deprecated in schema and docs.
- Measure usage.
- Contact affected consumers.
- Provide a support window.
- Remove only after agreed criteria.
One release-note line is not enough for external clients.
Use headers or schema metadata where supported.
Deprecation without a removal policy becomes permanent accumulation.
Usage telemetry
Before removing an operation, know:
- Which clients call it
- How often
- Which fields they request
- Which versions remain active
- Whether calls are production or test
Telemetry needs privacy and identity controls.
Anonymous public APIs may not identify every consumer, requiring longer support or explicit version deadlines.
Absence of observed traffic can be misleading if logging sampled or missed a path.
Use several evidence sources.
Contract testing
Provider tests verify that implementation matches the published contract.
Consumer-driven contract tests capture assumptions from known clients.
They can detect:
- Removed field
- Type change
- Status-code change
- Required interaction mismatch
Contract tests do not prove full end-to-end behavior, performance, or semantic correctness.
They are one layer alongside integration tests, examples, schema checks, and production telemetry.
Database changes and APIs
Schema migrations should not force an instantaneous API break.
Use expand-and-contract:
- Add new database field.
- Write old and new forms.
- Backfill data.
- Read from new form.
- migrate clients or API representation.
- Stop old writes.
- Remove old field later.
The API can preserve one stable representation while storage changes underneath.
Avoid exposing database migrations directly through public contracts.
Behavior changes
Suppose an API previously accepted invalid phone numbers and now rejects them.
The stricter behavior improves data quality but may break clients.
Options:
- Warn before enforcement
- Add validation endpoint
- Introduce compatibility mode
- Version the behavior
- Correct data in stages
Security fixes may require faster breaking changes.
Communicate impact, provide actionable migration, and choose safety over compatibility when harm demands it.
Error compatibility
Clients may depend on:
- Status code
- Error code
- Field path
- Retryability
Changing 404 to 200 with empty data is breaking.
Changing human message wording should not break clients if they use stable machine codes.
Design errors for compatibility from the beginning:
- Stable code
- Extensible details
- Unknown-code fallback
- Documented retry semantics
Error contracts deserve the same care as successful responses.
Pagination compatibility
Changing:
- Default sort
- Cursor encoding without opacity
- Page size
- Inclusion rules
- Snapshot behavior
can duplicate or skip records for active clients.
Cursors should be opaque and, when feasible, remain valid across compatible deployments.
If a migration invalidates them, return a clear restart signal.
Do not silently reinterpret an old cursor under a new ordering.
Supporting multiple versions
Each active version increases:
- Code paths
- Tests
- Documentation
- Security maintenance
- Monitoring
- Incident complexity
Strategies include:
- Separate implementations
- Translation layer into one internal model
- Shared core with version-specific adapters
Avoid scattering version conditionals throughout business logic.
Contain compatibility at boundaries where possible.
Set and publish support lifetimes that the organization can actually honor.
Client migration
A good migration guide includes:
- What changed
- Why
- Before and after examples
- Error differences
- Rollout timeline
- Test environment
- Verification checklist
- Support contact
For high-impact partners, provide usage reports and direct coordination.
Clients need time to develop, test, release, and wait for user adoption.
Mobile migration time includes app-store review and installed-version lag.
Compatibility and security
Old versions may rely on:
- Weak authentication
- Vulnerable algorithms
- Excessive data exposure
- Unsupported dependencies
Compatibility cannot justify indefinite unsafe behavior.
Use:
- Risk-based deadlines
- Restricted access
- Compensating controls
- Clear communication
- Forced upgrade where necessary
Security retirement should be planned before an emergency, with version ownership and end-of-life policy.
Knowledge check
- Why can changing an ID from a number to a string be widely breaking?
- Why can adding an enum value break a client?
- When should an API create a new version instead of evolving additively?
- What evidence should precede endpoint removal?
- How does expand-and-contract protect API compatibility during database migration?
The one idea to remember
API compatibility covers every behavior clients rely on, including semantics, errors, ordering, and timing. Evolve additively when possible, version meaningful breaks, measure real usage, and treat migration as a supported product process.