Practical guide
How to design API versioning
Add a new version only for a change that an existing client cannot safely accept.
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
- Treat removing or renaming a field, changing its type or meaning, making input stricter, and changing authentication or error behaviour as incompatible.
- Adding an optional field is usually compatible only if clients tolerate unknown fields. Verify this property with tests instead of assuming it.
- Judge bug fixes by observable behaviour. If a client depends on the old behaviour, even a fix may require a new version.
- Write the rules in repository documentation and review contract changes during code review and in CI.
2. Choose one way to address a version
- 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.
- 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.
- 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.
- 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
- 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.
- 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.
- Keep fixing the old version during the transition and monitor its traffic by identified client. Keep testing it until it is actually turned off.
- Announce retirement repeatedly and well in advance. Where appropriate, add the standard Sunset header and a link to migration documentation.
Step 4
Verify both versions as separate contracts
Every supported version must pass its own tests throughout the migration.
-
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' -
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 -
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.