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/usersFastAPI 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.pyEach 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 forgetMost 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
/v1from 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=Trueflag warns developers about retiring routes. - Shared logic lives in functions that every version calls.
