from datetime import datetime
from pydantic import BaseModel
class UserV1(BaseModel):
id: int
name: str
email: str
class UserV2(BaseModel):
id: int
first_name: str
last_name: str
email: str
created_at: datetime
from dataclasses import dataclass
from datetime import datetime
@dataclass
class DomainUser:
id: int
full_name: str
email: str
created_at: datetime
class UserService:
def get_user(self, user_id: int) -> DomainUser:
# Stand-in for a real repository lookup.
return DomainUser(
id=user_id,
full_name="Ada Lovelace",
email="ada@example.com",
created_at=datetime(1843, 10, 1),
)
def get_service() -> UserService:
return UserService()
from fastapi import APIRouter, Depends
from .schemas import UserV1, UserV2
from .service import UserService, get_service
v1_router = APIRouter(tags=["users:v1"])
v2_router = APIRouter(tags=["users:v2"])
@v1_router.get("/users/{user_id}", response_model=UserV1)
def read_user_v1(user_id: int, service: UserService = Depends(get_service)):
user = service.get_user(user_id)
return UserV1(id=user.id, name=user.full_name, email=user.email)
@v2_router.get("/users/{user_id}", response_model=UserV2)
def read_user_v2(user_id: int, service: UserService = Depends(get_service)):
user = service.get_user(user_id)
first, _, last = user.full_name.partition(" ")
return UserV2(
id=user.id,
first_name=first,
last_name=last,
email=user.email,
created_at=user.created_at,
)
from fastapi import FastAPI
from .routers import v1_router, v2_router
app = FastAPI(title="Users API")
app.include_router(v1_router, prefix="/api/v1")
app.include_router(v2_router, prefix="/api/v2")
@app.get("/health")
def health():
return {"status": "ok", "versions": ["v1", "v2"]}
This snippet shows how to run two versions of an API side by side in a single FastAPI application by defining each version's routes on its own APIRouter and mounting them under distinct path prefixes. The pattern keeps /v1 frozen for existing clients while /v2 evolves the response schema, without duplicating business logic or forking the service.
In schemas.py, two Pydantic models describe the wire format for each version. UserV1 exposes a single name field, reflecting the original contract. UserV2 splits that into first_name and last_name and adds created_at, a breaking change that justifies a new version. Keeping the schemas separate is the core discipline of versioning: the transport shape is decoupled from the internal domain, so a change to the API never silently reshapes stored data.
The shared domain lives in service.py. UserService.get_user returns a plain domain object that knows nothing about API versions. Both routers consume this one source of truth, so bug fixes and query logic are written once. The adapters that follow are responsible only for projecting that domain object into the correct versioned representation.
routers.py defines v1_router and v2_router, each an independent APIRouter with its own tags and its own response_model. The v1 handler maps the domain's full_name onto the flat UserV1.name, while the v2 handler splits the name and surfaces the timestamp. Both share the same get_service dependency, demonstrating that dependency injection composes cleanly across versions. Because each router owns its endpoint decorators, the two versions can diverge freely — new routes, different validation, deprecations — without touching each other.
main.py wires everything together with app.include_router, passing prefix="/api/v1" and prefix="/api/v2". FastAPI merges each router's paths under the prefix, so the same relative route lands at two URLs. This keeps the OpenAPI docs grouped by version via the tags. The main trade-off is duplicated adapter code as versions accumulate; the payoff is that old clients keep working untouched while new ones opt into the improved schema. A common pitfall is leaking shared mutable schema classes between versions — the deliberate separation in schemas.py avoids exactly that.
Related snips
package com.example.myapp
import android.app.Application
import dagger.hilt.android.HiltAndroidApp
@HiltAndroidApp
Dependency injection with Hilt
from django.urls import path
from . import views
app_name = 'blog'
urlpatterns = [
Django URL namespacing and reverse lookups
<?php
namespace App\Providers;
use App\Contracts\PaymentGateway;
use App\Services\StripePaymentGateway;
Laravel service container and dependency injection
from rest_framework import permissions
class IsOwner(permissions.BasePermission):
"""Allow only object owner to access."""
Django REST Framework permissions and authorization
import { Navigate, useLocation } from 'react-router-dom'
import { useAuth } from '@/contexts/AuthContext'
interface ProtectedRouteProps {
children: React.ReactNode
}
React Router with protected routes
Rails.application.routes.draw do
namespace :api do
namespace :v1 do
resources :posts do
resources :comments, only: [:index, :create]
end
Rails API versioning strategies
Share this code
Here's the card — post it anywhere.