FastAPI cookbook · Intermediate

Version a FastAPI API without breaking clients

Introduce breaking changes (renamed fields, new semantics) while existing clients keep working.

Approach

Mount versioned routers (/api/v1, /api/v2) that share services but have separate schemas. Keep v1 working, announce deprecation with Deprecation/Sunset headers and docs, monitor v1 usage per client, and remove it only after the sunset date.

Solution

from fastapi import APIRouter, Response

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


class UserOutV1(BaseModel):
    id: int
    name: str                       # v1: single name field


class UserOutV2(BaseModel):
    id: int
    first_name: str
    last_name: str


@v1.get("/users/{user_id}", deprecated=True)
async def get_user_v1(user_id: int, db: DB, response: Response) -> UserOutV1:
    response.headers["Deprecation"] = "@1767225600"            # RFC 9745 (Unix timestamp)
    response.headers["Sunset"] = "Wed, 30 Jun 2027 00:00:00 GMT"
    response.headers["Link"] = '</docs/migrate-to-v2>; rel="deprecation"'
    user = await users.get(db, user_id)
    return UserOutV1(id=user.id, name=f"{user.first_name} {user.last_name}")


@v2.get("/users/{user_id}")
async def get_user_v2(user_id: int, db: DB) -> UserOutV2:
    return await users.get(db, user_id)


app.include_router(v1)
app.include_router(v2)

Pitfalls

  • Versioning for additive changes (new optional fields don't need v2).
  • Duplicating business logic per version instead of sharing services.
  • Removing old versions without usage data or notice.

Variations

  • Header-based versioning (Accept: application/vnd.acme.v2+json).
  • Date-based versions like Stripe with per-account pinning.

Further reading: RFC 8594: The Sunset HTTP header

Reference: https://www.rfc-editor.org/rfc/rfc8594

More FastAPI recipes