Your APIWhat is api?A set of rules that lets one program talk to another, usually over the internet, by sending requests and getting responses. is a contract. Every client that calls your endpoints, frontend apps, mobile apps, third-party integrations, depends on your responses having specific fields with specific types. When you need to change that contract, you face a choice: break everyone at once, or version your API so old and new clients can coexist. AI generates versioning code quickly, but the strategy behind it requires human judgment.
What counts as a breaking changeWhat is breaking change?A modification to an API that causes existing code using it to stop working, such as renaming a field or changing a response format.
Before you version anything, you need to know what actually breaks clients. Not every change is dangerous.
Breaking changes (require a new version)
| Change | Why it breaks clients |
|---|---|
| Remove a field from a response | Clients expecting that field get undefined/null |
Rename a field (username to user_name) | Same as removing, old name is gone |
Change a field's type (price: int to price: str) | Client parsing fails |
| Change the URL path | Every client's requests go to a 404 |
| Add a required field to a request body | Existing clients do not send it, they get 422 errors |
| Change error response format | Client error handling breaks |
Non-breaking changes (safe without versioning)
| Change | Why it is safe |
|---|---|
| Add a new optional field to a response | Clients ignore unknown fields |
| Add a new endpoint | No existing client calls it |
| Add an optional field to a request body | Existing requests still valid |
| Improve error messages (same format) | Clients parse the format, not the text |
| Performance improvements | Faster is never breaking |
URL prefix versioning
The most common versioning strategy is URL prefixes. It is straightforward, visible, and what AI generates by default:
# routers/v1/users.py
from fastapi import APIRouter
router = APIRouter()
@router.get("/users/{user_id}")
def get_user_v1(user_id: int):
user = find_user(user_id)
return {
"id": user.id,
"username": user.username, # Original field name
"email": user.email,
"created_at": str(user.created_at), # String format
}# routers/v2/users.py
from fastapi import APIRouter
router = APIRouter()
@router.get("/users/{user_id}")
def get_user_v2(user_id: int):
user = find_user(user_id)
return {
"id": user.id,
"display_name": user.display_name, # Renamed field
"email": user.email,
"created_at": user.created_at, # ISO datetime object
"avatar_url": user.avatar_url, # New field
}# main.py
from fastapi import FastAPI
from routers.v1 import users as users_v1
from routers.v2 import users as users_v2
app = FastAPI()
app.include_router(users_v1.router, prefix="/v1", tags=["V1"])
app.include_router(users_v2.router, prefix="/v2", tags=["V2"])Now /v1/users/42 returns the old shape and /v2/users/42 returns the new shape. Existing clients keep working. New clients use the new version.
The backward compatibility trap
Here is where AI-generated versioning goes wrong most often. The version prefix is the easy part. The hard part is what happens behind the endpointWhat is endpoint?A specific URL path on a server that handles a particular type of request, like GET /api/users..
Consider this scenario: you renamed the database column from username to display_name. AI updates the V2 endpoint and the database model, but the V1 endpoint now looks like this:
# routers/v1/users.py - BROKEN after migration
@router.get("/users/{user_id}")
def get_user_v1(user_id: int):
user = find_user(user_id)
return {
"id": user.id,
"username": user.username, # Column no longer exists!
"email": user.email,
}The V1 endpoint still references user.username, but the database column is now display_name. The V1 endpoint crashes. Every existing client breaks, which is exactly what versioning was supposed to prevent.
The correct approach:
# routers/v1/users.py - CORRECT after migration
@router.get("/users/{user_id}")
def get_user_v1(user_id: int):
user = find_user(user_id)
return {
"id": user.id,
"username": user.display_name, # Map new column to old field name
"email": user.email,
}The V1 endpoint reads from the new column but returns it under the old field name. The contract with existing clients is preserved. This mapping layer is what AI almost always forgets.
Version deprecationWhat is deprecation?Marking a feature or API version as outdated and scheduled for removal, giving users time to switch to the replacement. strategy
Versioning is not free. Every active version is code you maintain, test, and monitor. You need a plan for retiring old versions.
from fastapi import APIRouter, Header
from fastapi.responses import JSONResponse
import warnings
router = APIRouter()
@router.get("/users/{user_id}")
def get_user_v1(user_id: int):
user = find_user(user_id)
response = JSONResponse(content={
"id": user.id,
"username": user.display_name,
"email": user.email,
})
# Warn clients this version is going away
response.headers["Deprecation"] = "true"
response.headers["Sunset"] = "2025-06-01"
response.headers["Link"] = '</v2/users>; rel="successor-version"'
return responseThe Deprecation, Sunset, and Link headers are standard HTTPWhat is http?The protocol browsers and servers use to exchange web pages, API data, and other resources, defining how requests and responses are formatted. headers that tell clients:
- This version is deprecated
- It will be removed on a specific date
- Here is the replacement URL
A responsible deprecation timeline:
- Announce deprecation: add headers, update docs, notify APIWhat is api?A set of rules that lets one program talk to another, usually over the internet, by sending requests and getting responses. consumers
- Monitor usage: track how many requests still hit the old version
- Grace period: give clients 3-6 months minimum to migrate
- Remove: only when traffic is near zero
OpenAPIWhat is openapi?A standard format for describing REST APIs - their endpoints, parameters, and response shapes. Tools can generate documentation and client libraries from it automatically. documentation
FastAPI auto-generates interactive APIWhat is api?A set of rules that lets one program talk to another, usually over the internet, by sending requests and getting responses. documentation at /docs (Swagger UI) and /redoc (ReDoc). This is one of FastAPI's best features, and it works with zero configuration.
app = FastAPI(
title="My API",
description="A production API with proper versioning",
version="2.0.0",
docs_url="/docs",
redoc_url="/redoc",
)Improving auto-generated docs
The auto-generated docs work, but they are bare-bones. AI generates the endpoints; you need to add the human context.
@router.get(
"/users/{user_id}",
response_model=UserResponse,
summary="Get a user by ID",
description="Retrieve a user's public profile. Returns 404 if the user does not exist.",
responses={
404: {"description": "User not found"},
200: {
"description": "User profile",
"content": {
"application/json": {
"example": {
"id": 42,
"display_name": "alice",
"email": "alice@example.com",
}
}
}
}
}
)
def get_user(user_id: int):
...Tags for organization
Tags group endpoints in the docs. Use them to organize by domain, not by version:
app.include_router(
users_v2.router,
prefix="/v2/users",
tags=["Users"],
)
app.include_router(
items_v2.router,
prefix="/v2/items",
tags=["Items"],
)You can also add tag descriptions:
tags_metadata = [
{"name": "Users", "description": "User registration, profiles, and authentication"},
{"name": "Items", "description": "CRUD operations for items in the marketplace"},
]
app = FastAPI(openapi_tags=tags_metadata)Alternative versioning strategies
URL prefix versioning is the most common, but not the only option:
| Strategy | Example | Pros | Cons |
|---|---|---|---|
| URL prefix | /v1/users | Visible, simple, cacheable | URL changes between versions |
| Header | Accept: application/vnd.api.v1+json | Clean URLs | Hard to test in browser |
| Query parameter | /users?version=1 | Easy to switch | Messy, caching issues |
| No versioning | Evolve carefully, never break | Simplest if possible | One mistake breaks everyone |
For most projects, URL prefix versioning is the right choice. It is explicit, easy to understand, easy to route, and easy to monitor in access logs. AI defaults to it for good reason.
When you do not need versioning
Not every APIWhat is api?A set of rules that lets one program talk to another, usually over the internet, by sending requests and getting responses. needs versioning from day one. If your API has a single consumer (your own frontend), you can deploy both at the same time and avoid the complexity entirely. Versioning matters when:
- External clients depend on your API (third parties, mobile apps with slow update cycles)
- Multiple teams consume your API and cannot all update simultaneously
- Public API where you do not control the consumers
If you control both the API and its only consumer, coordinated deployments are simpler than maintaining multiple versions. AI will add versioning scaffoldingWhat is scaffolding?Auto-generating the basic file structure and starter code for a project or feature so you don't have to write it from scratch. regardless, knowing when to strip it out is part of evaluating the generated code.