FastAPI API Versioning

An API behaves like a contract. Mobile apps, websites, and partner systems depend on the exact shape of every response. A change that renames a field can break every app that reads it. Versioning lets you ship new behavior under a new label while old clients keep working. Think of a phone charger standard: new models arrive, and old cables continue to fit older phones.

When a Change Needs a New Version

Breaking changes demand a new version:

  • Renaming or removing a field in a response.
  • Changing a field's type, such as text to number.
  • Making an optional parameter required.
  • Changing the meaning of an existing status code.

Adding a new optional field is safe and needs no new version.

URL Path Versioning

The most popular style puts the version in the address. Clients see it at a glance.

https://api.example.com/v1/users
https://api.example.com/v2/users

FastAPI routers make this style easy. Each version gets its own router with a prefix.

from fastapi import APIRouter, FastAPI

app = FastAPI()

v1 = APIRouter(prefix="/v1", tags=["v1"])
v2 = APIRouter(prefix="/v2", tags=["v2"])

@v1.get("/users/{user_id}")
def get_user_v1(user_id: int):
    return {"id": user_id, "name": "Asha Rao"}

@v2.get("/users/{user_id}")
def get_user_v2(user_id: int):
    return {"id": user_id, "first_name": "Asha", "last_name": "Rao"}

app.include_router(v1)
app.include_router(v2)
GET /v1/users/5  ──▶  {"id": 5, "name": "Asha Rao"}
GET /v2/users/5  ──▶  {"id": 5, "first_name": "Asha", "last_name": "Rao"}

Version 2 splits the name into two fields. Clients on version 1 keep receiving the old shape.

Folder Structure for Versions

app/
├── main.py
└── api/
    ├── v1/
    │   ├── users.py
    │   └── orders.py
    └── v2/
        ├── users.py
        └── orders.py

Each folder owns its routers and models. Teams can improve version 2 without touching version 1 code.

Header Versioning

Some teams keep addresses clean and ask clients to send the version in a header.

from fastapi import Header

@app.get("/users/{user_id}")
def get_user(user_id: int, api_version: str = Header(default="1")):
    if api_version == "2":
        return {"id": user_id, "first_name": "Asha", "last_name": "Rao"}
    return {"id": user_id, "name": "Asha Rao"}

Header versioning keeps one address for each resource. Testing from a browser address bar becomes harder, because browsers cannot add custom headers by typing a URL.

Comparing the Styles

Style          Example                  Strength            Weakness
────────────   ──────────────────────   ─────────────────   ─────────────────────
URL path       /v1/users                clear, easy to test   longer addresses
Header         api-version: 2           clean addresses       harder to test
Query string   /users?version=2         simple to add         easy to forget

Most public APIs choose URL path versioning for its clarity.

Marking Old Routes as Deprecated

Warn developers before you remove a version. The deprecated flag adds a visible strike-through in the interactive docs.

@v1.get("/users/{user_id}", deprecated=True)
def get_user_v1(user_id: int):
    return {"id": user_id, "name": "Asha Rao"}

Announce a retirement date in your documentation and give clients months to move.

Mounting Separate Apps

Large projects sometimes run each version as its own FastAPI app.

main_app = FastAPI()
main_app.mount("/v1", app_v1)
main_app.mount("/v2", app_v2)

Every mounted app owns its documentation page, so /v1/docs and /v2/docs stay separate.

Good Habits

  • Start with /v1 from day one, even before a second version exists.
  • Share business logic between versions through plain functions.
  • Avoid copying large blocks of code; wrap shared logic once.
  • Retire old versions on a published schedule.

A Realistic Migration Story

A small shop runs an API that returns each customer's full name in one field. The mobile app, the website, and a delivery partner all read that field. A new feature needs first and last names apart, so the team publishes version 2 with two fields. Version 1 stays online unchanged.

The website team upgrades within a week. The delivery partner needs two months, because their release cycle moves slowly. Version 1 keeps serving that partner until their upgrade ships. The shop announces a retirement date, watches the traffic dashboard, and switches off version 1 once the numbers drop to zero.

The story shows the real value of versioning: it buys time for every client to move at a comfortable pace. Without versions, one rename would have broken three products on the same morning.

Key Points

  • Versions protect existing clients from breaking changes.
  • Routers with prefixes implement URL path versioning.
  • Header and query versioning offer alternatives.
  • The deprecated=True flag warns developers about retiring routes.
  • Shared logic lives in functions that every version calls.