Advanced FastAPI Patterns • Lesson 10

API Versioning & Router Composition

Learn to version your API using APIRouter with prefixes and tags, composing multiple versioned routers into a single application.

🎯 What You'll Learn

  • Understand different API versioning strategies and when to use URL-based versioning
  • Create separate APIRouter instances for each API version
  • Use prefix and tags parameters to organize versioned routes
  • Compose multiple routers into a single FastAPI application with include_router

API Versioning & Router Composition

🎯 What You'll Learn

  • Different strategies for versioning APIs (URL, header, query parameter)
  • How to use APIRouter with prefix and tags for modular route organization
  • How to compose multiple routers into a single FastAPI application
  • Best practices for backward compatibility and deprecation

📚 Theory

As your API evolves, you will inevitably need to make breaking changes -- changing response formats, renaming fields, or restructuring endpoints. API versioning allows you to introduce these changes without breaking existing clients. FastAPI's APIRouter makes it straightforward to implement URL-based versioning by composing multiple versioned routers into a single application.

Why Version Your API?

Without versioning, any change to your response format or endpoint behavior breaks all existing clients. Versioning lets you:

  • Introduce breaking changes safely alongside the old version
  • Give clients time to migrate to the new version
  • Maintain multiple versions simultaneously during transition periods
  • Deprecate old versions on a published schedule

Versioning Strategies

There are three common approaches to API versioning:

1. URL Path Versioning (Recommended for most cases)

The version is part of the URL path. This is the most visible and explicit approach.

GET /api/v1/items/
GET /api/v2/items/

Advantages: Easy to understand, cache-friendly, simple to route. Disadvantages: URL changes between versions, requires client URL updates.

2. Header-Based Versioning

The version is specified in a custom request header.

GET /api/items/
Accept-Version: v2

Advantages: Clean URLs, version is metadata not resource identity. Disadvantages: Harder to test in browser, less discoverable.

3. Query Parameter Versioning

The version is a query parameter.

GET /api/items/?version=2

Advantages: Easy to test, no URL or header changes needed. Disadvantages: Can conflict with other parameters, less semantic.

APIRouter: The Building Block

APIRouter works like a mini-FastAPI application. It can have its own endpoints, dependencies, and middleware. The key parameters for versioning are prefix and tags.

from fastapi import APIRouter

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

@v1_router.get("/items/")
async def get_items():
    return [{"id": 1, "name": "Laptop"}]

The prefix parameter prepends a path to all routes on the router. The tags parameter groups endpoints in the OpenAPI documentation.

Composing Routers with include_router

Once you have defined your versioned routers, include them in the main application:

from fastapi import FastAPI

app = FastAPI(title="Versioned API")

app.include_router(v1_router)
app.include_router(v2_router)

include_router also accepts optional prefix, tags, and deprecated parameters that can override or extend the router's settings at include time.

Evolving Response Formats

A common pattern is to have v1 return simple data and v2 return enriched, paginated responses:

# V1: Simple list
@v1_router.get("/items/")
async def get_items_v1():
    return [{"id": 1, "name": "Laptop"}]

# V2: Enriched response with metadata
@v2_router.get("/items/")
async def get_items_v2():
    return {
        "data": [{"id": 1, "name": "Laptop", "category": "electronics"}],
        "total": 1,
        "version": "v2",
    }

The v2 format adds a data wrapper, item count, and version identifier. This pattern scales well as you add pagination, filtering metadata, or hypermedia links.

Backward Compatibility

When evolving your API, follow these guidelines:

  • Additive changes are safe: Adding new fields to responses does not break clients
  • Removing fields is breaking: Always version when removing response fields
  • Changing field types is breaking: Changing a field from string to integer requires a new version
  • New optional parameters are safe: Adding optional query or body parameters is backward-compatible

Deprecation Strategy

When retiring an old API version, follow these steps: announce the timeline to clients, mark routes as deprecated using deprecated=True in include_router, add deprecation headers to responses, monitor usage, and finally remove the old version after the deadline.

app.include_router(v1_router, deprecated=True)

🔧 Key Concepts

  • API Versioning: Managing multiple versions of your API simultaneously
  • URL Path Versioning: Embedding the version in the URL (e.g., /api/v1/)
  • APIRouter: A modular routing component with its own prefix, tags, and dependencies
  • prefix: Path prefix prepended to all routes on a router
  • tags: Labels that group endpoints in OpenAPI documentation
  • include_router: Method that mounts a router onto the main application
  • Backward Compatibility: Ensuring existing clients continue to work with API changes
  • Deprecation: The process of phasing out an old API version

💡 Best Practices

  • Use URL path versioning for public APIs -- it is the most explicit and discoverable approach
  • Extract shared business logic into service functions to avoid code duplication between versions
  • Only create a new version when you have a genuinely breaking change
  • Mark deprecated versions clearly in your OpenAPI documentation
  • Give clients a reasonable migration window (typically 6-12 months) before removing old versions
  • Include a version identifier in enriched responses so clients can verify they are hitting the right version
  • Use tags consistently to organize endpoints in the auto-generated docs

🔗 Additional Resources

💡 Hint

Create separate APIRouter instances with prefix='/api/v1' and prefix='/api/v2'. Define your endpoints on the routers, then call app.include_router() for each one. The v2 router should return richer response objects with 'data', 'total', and 'version' fields.

Ready to Practice?

Now that you understand the theory, let's put it into practice with hands-on coding!

Start Interactive Lesson