API Versioning: Strategies That Won't Break Your Clients
Implement API versioning using URL paths, headers, or query parameters while maintaining backward compatibility and managing deprecation gracefully.
February 11, 2026 · 7 min · 1302 words · Rob Washington
Table of Contents
APIs evolve. Fields get renamed, endpoints change, entire resources get restructured. The question isn’t whether to version your API—it’s how to do it without breaking every client that depends on you.
fromfastapiimportFastAPI,APIRouterapp=FastAPI()# Version 1v1_router=APIRouter(prefix="/api/v1")@v1_router.get("/users/{user_id}")asyncdefget_user_v1(user_id:int):return{"id":user_id,"name":"John Doe","email":"john@example.com"}# Version 2 - restructured responsev2_router=APIRouter(prefix="/api/v2")@v2_router.get("/users/{user_id}")asyncdefget_user_v2(user_id:int):return{"id":user_id,"profile":{"name":"John Doe","email":"john@example.com"},"metadata":{"created_at":"2026-01-01T00:00:00Z","updated_at":"2026-02-11T00:00:00Z"}}app.include_router(v1_router)app.include_router(v2_router)
Pros: Explicit, easy to understand, easy to route
Cons: URLs change between versions, harder to maintain multiple versions
fromfastapiimportFastAPI,Request,HTTPExceptionapp=FastAPI()defget_api_version(request:Request)->int:"""Extract version from Accept header."""accept=request.headers.get("Accept","")# Parse: application/vnd.myapi.v2+jsonif"vnd.myapi.v"inaccept:try:version_str=accept.split("vnd.myapi.v")[1].split("+")[0]returnint(version_str)except(IndexError,ValueError):pass# Default to latest stable versionreturn1@app.get("/api/users/{user_id}")asyncdefget_user(user_id:int,request:Request):version=get_api_version(request)ifversion==1:return{"id":user_id,"name":"John Doe"}elifversion==2:return{"id":user_id,"profile":{"name":"John Doe"},"metadata":{"version":"2"}}else:raiseHTTPException(status_code=400,detail=f"Unknown API version: {version}")
Pros: Clean URLs, version is metadata not resource identifier
Cons: Less discoverable, harder to test in browser
fromstarlette.middleware.baseimportBaseHTTPMiddlewarefromstarlette.requestsimportRequestclassVersionMiddleware(BaseHTTPMiddleware):asyncdefdispatch(self,request:Request,call_next):# Extract version from various sourcesversion=(self._from_path(request.url.path)orself._from_header(request.headers)orself._from_query(request.query_params)or1# default)# Add to request staterequest.state.api_version=versionresponse=awaitcall_next(request)response.headers["X-API-Version"]=str(version)returnresponsedef_from_path(self,path:str)->int|None:importrematch=re.search(r'/v(\d+)/',path)returnint(match.group(1))ifmatchelseNonedef_from_header(self,headers)->int|None:version_header=headers.get("X-API-Version")returnint(version_header)ifversion_headerelseNonedef_from_query(self,params)->int|None:version=params.get("version")returnint(version)ifversionelseNoneapp.add_middleware(VersionMiddleware)
fromfastapiimportFastAPIfromfastapi.openapi.utilsimportget_openapiapp=FastAPI()defcustom_openapi():ifapp.openapi_schema:returnapp.openapi_schemaopenapi_schema=get_openapi(title="My API",version="2.0.0",description="""
## API Versions
### v2 (Current)
- Restructured user response with `profile` and `metadata` objects
- Added pagination to list endpoints
### v1 (Deprecated - Sunset: June 1, 2026)
- Legacy flat response structure
- No pagination support
## Migration Guide
See [migration docs](/docs/migration-v1-to-v2) for upgrade instructions.
""",routes=app.routes,)app.openapi_schema=openapi_schemareturnapp.openapi_schemaapp.openapi=custom_openapi
Use URL versioning for public APIs — most discoverable
Support at least N-1 — give clients time to migrate
Communicate deprecation early — minimum 6 months notice
Track version usage — know who needs to migrate
Make migration easy — provide guides and tools
Version the whole API, not individual endpoints — consistency matters
API versioning is about respect for your clients. They built systems depending on your contract. Change thoughtfully, communicate clearly, and give them time to adapt.
📬 Get the Newsletter
Weekly insights on DevOps, automation, and CLI mastery. No spam, unsubscribe anytime.