7.19 API Versioning
Table of Contents
Why API Versioning Matters
APIs change over time. You fix bugs, add fields, tweak behavior, or even redesign parts of your system. If you change an API that real clients already use, you can easily break them.
API versioning is how you change an API without breaking existing clients. You keep old behavior available under one version, and expose new behavior under another.
Typical situations where you need a new version:
- You remove or rename a field in a response.
- You change the meaning or type of a field, for example
pricebecomes a string like"10.00 USD". - You change how validation works, for example a field that used to be optional becomes required.
- You redesign endpoints, for example splitting
/usersinto/customersand/admins.
Important rule: Whenever a change can break existing clients, you should treat it as a breaking change and consider a new API version.
Backward compatible changes (like adding a new optional response field) often do not require a new version. More on that later.
Backward Compatibility and Breaking Changes
Before talking about concrete versioning strategies, it is critical to understand the difference between backward compatible changes and breaking changes.
Backward Compatible Changes
A change is backward compatible if old clients can still use the API without any modifications.
Examples of backward compatible changes:
- Adding a new optional response field:
{
"id": 1,
"email": "john@example.com",
"is_active": true // new field
}Old clients that ignore unknown fields keep working.
- Adding a new endpoint:
- Old clients do not call it, so they are not affected.
- New clients can start calling it.
- Adding a new optional query parameter that has a safe default:
Old clients that do not send it behave as before.
- Improving error messages while keeping the status codes and basic error structure the same.
Because these changes do not break existing clients, you typically do not bump the API version just for them.
Breaking Changes
A breaking change is any change that can make existing clients fail, return unexpected data, or behave incorrectly.
Common breaking changes:
- Removing a field from a response:
If some clients rely on username, then:
// old
{ "id": 1, "username": "john" }
// new
{ "id": 1 }can break them.
- Changing a field type:
// old
{ "price": 10.0 }
// new
{ "price": "10.00 USD" }- Renaming fields:
From first_name to given_name.
- Changing response structure:
// old
{ "user": { "id": 1, "email": "john@example.com" } }
// new
{ "id": 1, "email": "john@example.com" }- Changing a success status code to an error code or vice versa, for example from
200 OKto204 No Contentor201 Createdto400 Bad Requestin some scenarios. - Making an input required that used to be optional.
- Changing semantics, for example:
status: "active"used to mean "paid user", now means "email verified user".GET /usersused to return all users, now returns only active ones.
Rule of thumb:
If a client written today can break tomorrow without changing its own code, then your change is breaking and should be handled using versioning.
Common API Versioning Strategies
There is no single "correct" way to version APIs. Different companies and frameworks choose different approaches. You should know the most common ones, their pros and cons, and how they look in real requests.
Here are the main strategies:
| Strategy type | Example |
|---|---|
| URI path versioning | /api/v1/users |
| Query parameter | /api/users?version=1 |
| Header versioning | Accept: application/vnd.app.v1+json |
| Content negotiation | Accept: application/vnd.app.user.v2+json |
We will focus on the three most common in REST APIs: URI path, query parameter, and header.
1. URI Path Versioning
The version is part of the URL path.
Examples:
GET /api/v1/usersGET /api/v2/usersPOST /v1/ordersPOST /v2/orders
How it works
You introduce new versions by exposing new URL paths. Old paths remain available.
Concrete example:
- Version 1:
GET /api/v1/usersreturns:
{
"id": 1,
"username": "john"
}- Version 2 changes the response to use
emailinstead ofusername: GET /api/v2/usersreturns:
{
"id": 1,
"email": "john@example.com"
}Clients that use v1 keep working. New clients can move to v2 when they are ready.
Pros
- Very simple to understand and use.
- Easy to see which version a client uses by looking at the URL.
- Easy to support multiple versions at the same time, for example keep both
/v1and/v2available.
Cons
- The version is part of the resource URL, but some people argue versions are not really part of the "resource identity".
- URLs change completely when upgrading, for example from
/v1/users/1to/v2/users/1. - If you have many versions, routing can become more complex or duplicated.
For beginners, URI path versioning is usually the easiest to start with.
2. Query Parameter Versioning
The version is passed as a query parameter.
Examples:
GET /api/users?version=1GET /api/users?api_version=2GET /orders?v=2
How it works
You keep the same base path and switch logic based on a query parameter.
Example:
- Version 1:
GET /api/users?version=1Response:
{
"id": 1,
"username": "john"
}- Version 2:
GET /api/users?version=2Response:
{
"id": 1,
"email": "john@example.com"
}Pros
- URLs are similar for all versions.
- Easy to experiment, for example test v2 in a browser with
?version=2. - Simple to implement in many frameworks.
Cons
- Less visible than path versioning, some developers might forget to include
versionin documentation or clients. - Caches and proxies might need extra configuration, because the same path with different query parameters can mean different versions.
- Not as common or standardized as path-based versioning in public APIs.
3. Header-Based Versioning
The version is provided through HTTP headers.
There are two popular variants:
- A custom header like
X-API-Version. - The
Acceptheader with a versioned media type.
3.1 Custom Header: `X-API-Version`
Example:
GET /api/users HTTP/1.1
Host: api.example.com
X-API-Version: 1or
GET /api/users HTTP/1.1
Host: api.example.com
X-API-Version: 2Server uses the header to decide which version logic to apply.
Pros:
- URLs stay clean and stable.
- Easy to add to existing clients that already send headers.
Cons:
- Harder to test quickly in a browser unless your tools let you edit headers.
- Less visible when you only look at URLs.
3.2 Accept Header (Content Negotiation)
This uses content negotiation. The client specifies exactly what media type (format) it accepts, including version.
Example:
GET /api/users HTTP/1.1
Host: api.example.com
Accept: application/vnd.myapp.v1+jsonor
GET /api/users HTTP/1.1
Host: api.example.com
Accept: application/vnd.myapp.v2+json
The part application/vnd.myapp.v1+json is a vendor-specific media type.
Pros:
- Very flexible, you can version at the level of resource representations.
- Good for advanced APIs and when you want strict control over formats.
Cons:
- More complex for beginners.
- Clients must manage non-standard media types.
- Tools and browsers are less friendly for this approach.
For many beginner backends, path versioning or query parameter versioning will be simpler and more practical.
Designing Version Numbers
Once you choose how to send versions, you also need to decide what the version number means.
Common styles:
| Style | Examples | Typical meaning |
|---|---|---|
| Simple int | v1, v2 | Major versions only |
| SemVer-like | v1.2, v2.0 | Major and minor versions |
| Date-based | v2024-01-01 | Version corresponds to a release date |
For beginner REST APIs, a simple integer is usually enough, for example:
v1for the first stable version.v2when you introduce breaking changes.- Maybe
v3later.
Common practice:
Use v1, v2, v3 where each new version includes breaking changes compared to the previous one. Backward compatible changes can be added inside the same version.
You can track minor non-breaking changes in documentation or internal release notes without changing the URL or version identifier.
When Should You Create a New Version?
Not every change needs a new version. Knowing when to create a new version is as important as knowing how.
Changes That Usually Do Not Need a New Version
These are backward compatible, so you normally stay within the same version:
- Adding new endpoints, for example adding
GET /api/v1/reports. - Adding new optional response fields that clients can ignore.
- Adding new query parameters that are optional and have safe defaults.
- Fixing bugs where the old behavior was clearly incorrect.
- Improving performance without changing observable behavior.
- Adding more allowed values when it does not break clients, for example:
- Status used to be
["pending", "done"], now also"canceled".
Old clients that do not know"canceled"may break though, so be careful.
In some cases you still need to communicate these changes, even if you do not bump version.
Changes That Usually Require a New Version
These are breaking and should almost always create a new version:
- Removing an endpoint or specific path.
- Renaming or removing fields.
- Changing types of fields, for example
stringtonumber. - Changing default behavior in a way that might surprise clients, for example:
- Old:
GET /reportsreturns all. - New:
GET /reportsreturns only last month. - Making previously accepted input invalid.
- Changing contract for error responses, for example:
- Old: error responses were
{"detail": "message"}. - New: they are
{"error": {"code": 123, "message": "msg"}}.
Example:
- Version 1 response:
{
"id": 1,
"username": "john",
"age": 30
}- You want to switch to
full_nameandbirth_year, and removeage. This is a breaking change.
Better approach:
- Keep
v1as is. - Create
v2with new structure:
{
"id": 1,
"full_name": "John Doe",
"birth_year": 1994
}- Give clients time to migrate from
v1tov2.
Supporting Multiple API Versions
Once you introduce a new version, you often need to support multiple versions simultaneously. Old clients stay on the old version until they are updated.
Parallel Version Support
Common approaches:
- Keep separate endpoints or routing sections:
/api/v1/...and/api/v2/...- In code, maybe separate controllers, routers, or modules for each version.
- Share logic where possible, but allow differences where needed.
Example structure in pseudocode:
app/
api/
v1/
users.py
orders.py
v2/
users.py # new behavior
orders.py # same as v1 or modified
core/
models.py # shared data models
services.py # shared business logicVersion-specific code (like old and new response models) lives under each version folder. Shared logic that is not version-dependent lives in a shared location.
Deprecation and Sunsetting Old Versions
You probably do not want to support all versions forever. Common lifecycle:
- Release v2 next to
v1. - Mark
v1as deprecated in documentation. - Communicate a timeline for removal.
- After that date, turn off
v1or return clear error messages.
You can also send warnings:
- Include a header like:
Deprecation: true
Sunset: Wed, 01 Jan 2025 00:00:00 GMT- Or include deprecation information in API documentation and release notes.
Even for beginner projects, it is useful to at least think about a plan:
- How long would you keep old versions?
- How will you inform users which version to use?
Handling Version-Specific Bugs
Sometimes a bug exists only in one version. You have a choice:
- Fix the bug in all versions.
- Fix only in newer versions, and keep old behavior in old versions if that behavior has become part of the "contract".
Example:
v1incorrectly rounds prices down instead of up.- You fix it in
v2. - You might decide to keep
v1behavior unchanged, and simply mark it as a known issue, while telling clients to upgrade if they care about accurate rounding.
The correct choice depends on how serious the bug is and how much clients rely on the current behavior.
Versioning at the Resource vs API Level
Sometimes you might want to version not the entire API, but only specific resources or responses.
For example, your API path might stay as /users, but you have different representations.
Using header-based versioning (content negotiation):
- To request version 1 of the user representation:
GET /users/1
Accept: application/vnd.myapp.user.v1+json- To request version 2:
GET /users/1
Accept: application/vnd.myapp.user.v2+jsonThis can be useful if:
- Most of the API is stable, but some endpoints are evolving quickly.
- You want to let clients choose on a per-resource basis which version they consume.
This approach is more advanced, and beginners often do not need it right away. It is good to know it exists and that versioning can happen at different granularity levels:
| Level | Example |
|---|---|
| Whole API | /api/v1/... vs /api/v2 |
| Group of endpoints | /api/v1/users/* only |
| Single resource | Accept: ...user.v2+json |
Best Practices for API Versioning
Finally, some general guidelines to keep your versioning strategy clean and understandable.
1. Choose One Main Strategy and Be Consistent
Do not mix several different versioning schemes randomly.
For example, choose:
GET /api/v1/...andGET /api/v2/...for all endpoints.
or
GET /api/...withX-API-Version: 1or2.
Being consistent helps both your users and your future self.
2. Start with `v1` Once the API Is Stable
During early experiments you may change the API often. You may keep it "unversioned" while you are still prototyping.
Once you have a stable API that other people can rely on, introduce v1 to signal stability. From there:
- Use
v1until you need breaking changes. - Then create
v2and so on.
3. Keep Changes Small and Documented
Instead of huge jumps from v1 to v3, make smaller, controlled changes:
- Add features in the same version when it is safe.
- Introduce a new version only when necessary.
- Document clearly what changed between versions:
- New endpoints.
- Changed fields.
- Deprecated behavior.
A simple version change log per endpoint can help:
GET /api/v1/users
- v1.0: initial version.
- v1.1: added "is_active" field (optional).
- v2.0: replaced "username" with "email".
Even if your URLs only show the major version v1 and v2, you still know what has changed inside each.
4. Design for Backward Compatibility When Possible
Try to evolve your API in a way that reduces the need for new versions:
- Avoid removing fields. Instead, mark them as deprecated and document that they will disappear in the future.
- Use new optional fields to extend behavior.
- When changing types or semantics, consider leaving old fields and adding new ones.
Example:
Instead of:
// old
{ "price": 10.0 }
// new
{ "price": "10.00 USD" }Consider:
// transitional
{
"price": 10.0, // deprecated
"price_with_currency": "10.00 USD"
}Then:
- Clients can migrate gradually to
price_with_currency. - Later, you can introduce a new version where
priceis removed.
5. Communicate Deprecation Early
If you plan to remove or change something that many clients use:
- Mark it as deprecated in documentation.
- Add comments to your OpenAPI or Swagger docs if you use them.
- Possibly add response headers that warn about deprecation.
For example, for a deprecated endpoint in v1, you might return:
Warning: 299 - "Deprecated API. Please migrate to /api/v2/users"6. Avoid Too Many Versions at the Same Time
Maintaining 5 or 6 active versions is complex and error-prone.
Better pattern:
- Keep at most 2 or 3 active versions.
- Encourage or require clients to upgrade regularly.
- Retire very old versions after some time.
For small or personal projects, you might keep only 1 or 2 versions.
7. Test Each Version Separately
Each version is effectively a separate contract.
- Write tests for
v1behavior andv2behavior independently. - Make sure you do not accidentally change
v1when updatingv2.
Simple example idea:
test_get_users_v1_returns_usernametest_get_users_v2_returns_email
If you change something, these tests can quickly show which version is affected.
Summary
API versioning helps you:
- Change and improve your API safely.
- Avoid breaking existing clients.
- Introduce new behavior while keeping old behavior working.
Key ideas:
- Backward compatible changes usually do not need new versions.
- Breaking changes should be released as new versions.
- Common strategies:
- Path-based:
/api/v1/...,/api/v2/... - Query parameter:
?version=1 - Header-based:
X-API-VersionorAccept: application/vnd.app.v2+json - Choose a simple version format like
v1,v2, and be consistent. - Support multiple versions in parallel for some time, and clearly communicate deprecations.
- Document differences between versions and test each version separately.
With these principles you can design REST APIs that can evolve over time without surprising or breaking the people who use them.
Views: 10
KAHIBARO