← All posts
7 min read

API Versioning and Compatibility: Evolving Contracts Without Surprise

#technology#api-versioning#compatibility#api
📑 On this page

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/orders

Benefits:

  • 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+json

or 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-16

This 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:

  1. Announce replacement.
  2. Document migration.
  3. Mark deprecated in schema and docs.
  4. Measure usage.
  5. Contact affected consumers.
  6. Provide a support window.
  7. 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:

  1. Add new database field.
  2. Write old and new forms.
  3. Backfill data.
  4. Read from new form.
  5. migrate clients or API representation.
  6. Stop old writes.
  7. 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

  1. Why can changing an ID from a number to a string be widely breaking?
  2. Why can adding an enum value break a client?
  3. When should an API create a new version instead of evolving additively?
  4. What evidence should precede endpoint removal?
  5. 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.