X

API Versioning and Backward Compatibility: A Practical Guide

An API can keep returning valid JSON and still break a client. A changed default, a newly required parameter, or a response field that an older decoder rejects can stop an integration that was working yesterday.

The safest policy is to keep an existing API version compatible and make changes additive where possible. Create a new version when a change really is incompatible, then give consumers a documented way and enough time to migrate. This guide compares common versioning approaches and outlines a release process that a small team can maintain.

What backward compatibility means

Compatibility has more than one dimension. A change can affect whether client code compiles against a new SDK, whether an old client can exchange requests and responses with a new server, or whether the server still behaves as the client expects. Google's backward-compatibility guidance describes these as source, wire, and semantic compatibility. Even if you publish only an HTTP API, the same distinction helps find changes that a schema diff alone can miss.

Change Usually compatible? What to check
Add a new endpoint Often, if existing routes and shared resources behave the same Does it change authorization, quotas, or existing operations?
Add an optional request parameter Often, if omitting it preserves the old behavior Is the default exactly what existing clients expect?
Add a response field Often, but not for every client Can generated SDKs and strict JSON decoders ignore unknown fields?
Add a response enum value Not automatically Can clients handle values they do not recognize?
Remove or rename a field, route, or parameter No Existing clients may still send or read it
Make optional input required or change a field's type No Requests can fail to validate or decoded values can change
Change defaults, ordering, pagination, status codes, or error meaning It can be breaking Existing code may rely on the current behavior, even if it is not obvious in the schema

Treat this table as a review prompt, not a universal compatibility guarantee. A new response property may be harmless to one client and rejected by another. An enum value may be safe only when clients are designed to handle unknown values. Microsoft's Azure REST API guidelines call out versioning as a contract concern; their guidance also recommends extensible enums so that a new value does not automatically break consumers.

Pagination is a common semantic trap. Switching an endpoint from returning all items to returning one page changes what a client receives, even when every individual item has the same schema. See our guide to designing API pagination for small services before changing that behavior.

Common ways to identify an API version

There is no single versioning scheme that suits every API. Select one mechanism, apply it consistently, and document exactly how a request chooses a version.

Approach Example Tradeoffs
URI path /v1/widgets Easy to see in logs, documentation, and browser tools. Routing and caching are straightforward, but each major version can add route and support overhead.
Query parameter /widgets?api-version=2026-01-15 Visible in a URL and can distinguish cached responses. A client can accidentally omit it unless the server requires it or defines a safe default.
Dedicated request header X-Example-API-Version: 2 Keeps the resource URL stable and supports a single version choice across routes. It is less visible when debugging a URL, so clients and intermediaries must forward it and caches must distinguish versions.
Versioned media type Accept: application/vnd.example.widgets-v2+json Uses HTTP content negotiation and can fit changes to representation formats. It adds complexity to clients and caches; when responses vary by Accept, configure caching to distinguish those representations.

These are different design choices, not competing standards. HTTP defines Accept as a way for a client to express preferred response media types; it does not prescribe one API-versioning policy. If the response varies by a request header, configure shared caches to distinguish those representations as well. See RFC 9110 on content negotiation and the HTTP caching rules for Vary.

Established providers use different patterns:

  • The Microsoft Azure REST API guidelines require an api-version query parameter for Azure data-plane operations and use date-shaped version values. They explicitly advise against putting the version in the path. This is Azure-specific guidance, not a requirement for every REST API.
  • Google's API versioning guidance puts a major version in the REST path, such as /v1, and does not expose minor or patch numbers as API versions.
  • The GitHub REST API selects a dated version with the X-GitHub-Api-Version request header. GitHub documents a support period of at least 24 months for the previous version after a new one is released.
  • Stripe's API versioning policy separates monthly releases without breaking changes from major releases that may require integration updates. Its SDKs and webhook endpoints also have version-selection behavior worth checking when you build a similar event-driven integration.

For a new public REST API, a major version in the path is often the easiest choice for consumers to understand. If your platform already uses a required version header or query parameter, keeping that convention is usually less surprising than introducing another one. Whatever you choose, define what happens when the selector is missing or unsupported. Avoid silently treating an unversioned request as “latest” if that could change the response seen by an existing client.

Prefer compatible changes over new versions

For changes that do not alter existing behavior, keep the current version and publish the addition. For example, an optional query parameter can add a filter while requests that omit it continue to return the same results as before.

For response additions, document whether unknown properties and enum values can appear. Keep existing properties populated with their existing meaning. Do not assume every client uses a permissive JSON decoder: some generated clients or validation layers reject fields that were not in their schema.

Avoid changing existing defaults as a shortcut to a new feature. If a new response shape requires opt-in, use an explicit selector or add a new operation rather than changing the old operation for every client. A compatibility change is not safe just because it is additive in an OpenAPI document; check the behavior of actual request and response consumers.

A versioning workflow for a small team

1. Write down the current contract

Keep an OpenAPI document or another reviewable API description alongside the implementation. Record request and response fields, requiredness, defaults, status codes, errors, pagination behavior, authentication expectations, and any guarantees about ordering or retries.

If you already have a contract, save a released copy. Without a baseline, it is difficult to know whether a proposed change is additive or whether a client has started depending on an undocumented behavior.

2. Identify consumers and choose a support policy

List the clients you control, published SDKs, webhook consumers, and integrations that may deploy on their own schedules. If you have request telemetry, use it to identify which versions are still active; avoid collecting more customer data than the migration requires.

Before announcing a breaking change, set the new version identifier, the old version's support end date or review policy, and the migration window. There is no universal minimum window: a public API with infrequent client releases needs more time than an internal service deployed in lockstep. State the policy before consumers depend on the version.

3. Add the new contract without changing the old one

Keep the old version's behavior available while you implement the new contract. Where possible, have both versions operate on the same underlying resources rather than copying or splitting customer data. A versioned route such as /v1/widgets and /v2/widgets should normally identify the same logical widget, even if the representations differ.

Do not create a new major version for every feature. Use it when an incompatible change is necessary, such as removing a field, changing a required input, or changing the meaning of an existing response. If only one operation needs a new behavior, consider whether an additive operation or capability-specific endpoint can avoid a whole-API migration.

4. Publish a migration guide and announce the change

Explain what changed, who is affected, how to select the new version, and what code or data migration is required. Include before-and-after request and response examples, replacement fields or operations, SDK upgrade steps, and a rollback path where one exists. Update the API reference and examples at the same time as the implementation.

Tell consumers through the channels they already use: release notes, a developer changelog, email or dashboard notices for affected accounts, and direct contact for high-impact integrations. A response header is useful extra visibility, not a replacement for a migration plan.

The Deprecation header standard defines a way to signal that a resource is or will be deprecated and link to more information. The Sunset header can communicate when a resource is expected to become unresponsive. Deprecation itself does not change how an API behaves, and these headers do not notify every developer who needs to act.

5. Test old and new behavior

Keep version-specific contract tests in the same workflow as the implementation. A practical test set includes:

  • Old request examples still work against the old version and retain their existing defaults.
  • The new version accepts and returns its documented fields, status codes, and errors.
  • Existing clients can ignore optional response additions or safely handle unknown enum values.
  • Pagination, filtering, sorting, and authorization behave as documented in each version.
  • Webhook consumers can parse events produced for the version they selected.
  • SDKs built from the published schema continue to compile and send valid requests.

Run tests against the actual API implementation, not just a schema validator. A schema diff can flag removed fields, but it may not detect that the server changed an order, default, or business rule. Our guide to API testing and documentation workflows for small teams covers contract checks and keeping examples in sync.

6. Migrate, observe, then retire

During the overlap period, publish both versions and monitor aggregate use of the version being retired. Work with consumers who still send old requests, update supported SDKs, and keep the migration guide current when teams encounter problems.

Remove the old version only after the announced support period and the checks in your policy are satisfied. If you must make an emergency change for a security, privacy, or reliability issue, explain why the normal schedule cannot be followed and communicate the safest available migration path.

A practical default for an indie API

For a small product with external clients, use this policy:

  1. Keep each released version stable and make compatible additions without changing existing defaults.
  2. Choose one explicit version selector. A path such as /v1 is simple for a new REST API; follow an existing header or query convention if your service already has one.
  3. Keep old and new versions available together for a support window that matches how your customers deploy.
  4. Publish a dated deprecation notice, a migration guide, and updated API and SDK examples before retirement.
  5. Test old and new contracts in CI, then use telemetry and customer communication to verify that migration is actually happening.

The goal is not to avoid all change. It is to make the cost of change visible, keep existing integrations working while consumers decide when to upgrade, and give them a clear path when a breaking change is unavoidable.

Official sources

Categories: Development
Tags: API services
Related Post