Practical guide

How to design API versioning

Add a new version only for a change that an existing client cannot safely accept.

25 minutes · API

First, the short version

Version the contract, not the deployment

An API is a contract between a provider and a client. A backwards-compatible change lets an old client keep working; an incompatible change requires a new contract and a migration plan.

An endpoint does not need a new version for every server release. Separate major versions only when meaning, structure, or behaviour changes in a way that would break an existing client.

Get ready

What you need

Versioning works only when you know the current contract and its actual consumers.

  • A list of API clients, their owners, and the versions they currently use.
  • A machine-readable description of the current contract, such as OpenAPI, plus integration or contract tests.
  • A written definition of backwards-compatible and incompatible changes for your team.
  • A way to measure traffic by version and a communication channel for migration and retirement notices.

Steps 1 to 3

Introduce versions as a controlled contract change

Set the rules first, then choose how versions are addressed, and finally define the old version lifecycle.

1. Define an incompatible change

  1. Treat removing or renaming a field, changing its type or meaning, making input stricter, and changing authentication or error behaviour as incompatible.
  2. Adding an optional field is usually compatible only if clients tolerate unknown fields. Verify this property with tests instead of assuming it.
  3. Judge bug fixes by observable behaviour. If a client depends on the old behaviour, even a fix may require a new version.
  4. Write the rules in repository documentation and review contract changes during code review and in CI.
Official OpenAPI specification

2. Choose one way to address a version

  1. For a public HTTP API, a major version in the path such as /api/v1/orders is simple. It is visible in routes, logs, documentation, and caches.
  2. A header or media-type version can preserve the same URL, but is harder to try in a browser and must be included in the cache key with Vary.
  3. Choose one approach across the API and do not mix it between endpoints. Do not send a version in both path and header as two independent sources of truth.
  4. Keep only the major version in the URL. Internal application releases can continue at their own pace without changing the public contract.
php bin/console debug:router
RFC 9110: HTTP semantics

3. Operate a migration, not a permanent copy

  1. Separate v1 and v2 transport adapters, but share the application use case and domain rules. Do not copy the entire system for a different response shape.
  2. For the new version, publish the exact differences, examples, migration procedure, and old-version retirement date. A client must know what to change and by when.
  3. Keep fixing the old version during the transition and monitor its traffic by identified client. Keep testing it until it is actually turned off.
  4. Announce retirement repeatedly and well in advance. Where appropriate, add the standard Sunset header and a link to migration documentation.
RFC 8594: the Sunset header

Step 4

Verify both versions as separate contracts

Every supported version must pass its own tests throughout the migration.

  1. Check routes for both versions

    Every public endpoint must be available only under the intended version and HTTP method.

    php bin/console debug:router | grep '/api/v'
  2. Run contract tests for v1 and v2

    V1 must preserve the old shape and meaning while v2 verifies the new contract. Test shared business rules together.

    php bin/phpunit tests/Contract/Api
  3. Check the retirement notice

    For the old version, verify the headers and migration guide link, but only after the date has been publicly announced.

    curl -i https://api.example.test/api/v1/orders/42

If something goes wrong

Common problems

The API gets a new version for every release

Separate the internal deployment version from the public major contract version. Add compatible fields and new endpoints to the current version.

V1 and v2 contain two copies of business logic

Move the difference into input and output adapters. Share application operations and domain rules when their meaning has not actually changed.

A compatible change broke an old client

The client probably rejects unknown fields or depends on undocumented behaviour. Add its case to contract tests and refine the compatibility rules.

php bin/phpunit tests/Contract/Api/V1
The old version can never be retired

Measure usage by client, assign a migration owner, and set a specific date. Retirement cannot be managed without contacts, telemetry, and a communication plan.

Done

API versions have a clear lifecycle.

The API now distinguishes compatible extensions from changes that require a new contract. Open the next major version only with a migration plan, telemetry, and a retirement date for the old one.

Request a call

I will call you on the next working day between 9:00 and 17:00.

You can also call me directly.

+420 605 181 728

Leave your phone number and send a callback request.

By sending, you agree to processing your data in order to handle your request.