FastAPI/
Lesson

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)

ChangeWhy it breaks clients
Remove a field from a responseClients 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 pathEvery client's requests go to a 404
Add a required field to a request bodyExisting clients do not send it, they get 422 errors
Change error response formatClient error handling breaks

Non-breaking changes (safe without versioning)

ChangeWhy it is safe
Add a new optional field to a responseClients ignore unknown fields
Add a new endpointNo existing client calls it
Add an optional field to a request bodyExisting requests still valid
Improve error messages (same format)Clients parse the format, not the text
Performance improvementsFaster is never breaking
AI pitfall
AI does not distinguish between breaking and non-breaking changes. If you ask it to "add a field to the user response," it sometimes renames existing fields to be "more consistent", breaking every client in the process. Always diff the before and after to catch unintended renames or removals.
02

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.

03

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.

AI pitfall
AI treats versioning as a URL routing problem. It creates the folder structure and the prefixes, but it does not maintain the data mapping between versions. After any database migration, you must verify that all active API versions still return the correct response shape.
04

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 response

The 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:

  1. 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
  2. Monitor usage: track how many requests still hit the old version
  3. Grace period: give clients 3-6 months minimum to migrate
  4. Remove: only when traffic is near zero

05

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)
06

Alternative versioning strategies

URL prefix versioning is the most common, but not the only option:

StrategyExampleProsCons
URL prefix/v1/usersVisible, simple, cacheableURL changes between versions
HeaderAccept: application/vnd.api.v1+jsonClean URLsHard to test in browser
Query parameter/users?version=1Easy to switchMessy, caching issues
No versioningEvolve carefully, never breakSimplest if possibleOne 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.

07

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.