Skip to main content

Native, router-based API versioning for FastAPI, featuring per-version docs and a declarative route lifecycle.

Project description

FastAPI Router Versioning (Native API Versioning)

PyPI License: MIT Python

The native, router-based solution for API versioning in FastAPI.

If you want to implement FastAPI API versioning, you can simply annotate your routes with @api_version, call .versionize(), and get isolated URL prefixes, per-version Swagger UI, and a full route lifecycle — all without touching your existing application structure.


Features

  • SemVer and CalVer — version routes with (major, minor) tuples or arbitrary strings
  • Per-version docs — isolated Swagger UI, ReDoc, and openapi.json for every version
  • Declarative lifecycle — introduce, deprecate, and remove routes with a single decorator
  • Latest alias — serve the newest version under a stable /latest prefix
  • Self-hosted docs — point Swagger UI and ReDoc at your own assets for air-gapped environments
  • Reverse proxy aware — doc URLs include the ASGI root_path at request time, so sub-app mounting works out of the box
  • Broad compatibility — works with nested routers, WebSockets, Depends, and OpenAPI Callbacks

Requirements

  • Python ≥ 3.10
  • FastAPI ≥ 0.120.0

Installation

pip install fastapi-router-versioning
# or
uv add fastapi-router-versioning

Quick start — SemVer

from fastapi import APIRouter, FastAPI
from fastapi_router_versioning import RouterVersioner, VersionFormat, api_version

app = FastAPI()
router = APIRouter()


@router.get("/items")
@api_version((1, 0))
def get_items_v1():
    return {"version": "1.0", "items": ["a", "b"]}


@router.get("/items")
@api_version((2, 0))
def get_items_v2():
    return {"version": "2.0", "items": ["a", "b", "c"]}


RouterVersioner(app=app, routers=router, version_format=VersionFormat.SEMVER).versionize()
# Mounts: GET /v1_0/items   GET /v2_0/items

Each version also gets its own Swagger UI at /v1_0/docs, /v2_0/docs, and so on.


Quick start — CalVer

from fastapi import APIRouter, FastAPI
from fastapi_router_versioning import RouterVersioner, VersionFormat, api_version

app = FastAPI()
router = APIRouter()


@router.get("/items")
@api_version("2025-01-01")
def get_items():
    return {"release": "2025-01-01"}


RouterVersioner(app=app, routers=router, version_format=VersionFormat.CALVER).versionize()
# Mounts: GET /2025-01-01/items

Any string is a valid CalVer token: "2025-01-01", "v3", "stable", etc.

CalVer sorting: versions are sorted lexicographically, so strings must be comparable in the intended order. ISO dates ("2025-01-01") and zero-padded numbers ("v01", "v02") work correctly. Strings like "v1", "v10", "v2" will not sort correctly and will cause routes to appear in the wrong versions.


Route lifecycle

Use deprecate_in and remove_in to manage the full lifecycle of a route across versions.

@router.get("/legacy")
@api_version((1, 0), deprecate_in=(2, 0), remove_in=(3, 0))
def legacy_route():
    return {"msg": "I am stable in v1, deprecated in v2, gone in v3."}
Version /legacy present? Marked deprecated?
v1.0 yes no
v2.0 yes yes
v3.0 no

Routes without @api_version fall back to default_version (default: (1, 0) for SemVer, "1" for CalVer).


RouterVersioner reference

Parameter Type Default Description
app FastAPI required The FastAPI application instance
routers APIRouter | list[APIRouter] required Router(s) whose routes will be versioned
version_format VersionFormat SEMVER Versioning strategy (SEMVER or CALVER)
prefix_format str | None /v{major}_{minor} / /{version} URL prefix template; supports {major}, {minor}, {version}
semantic_version_format str | None {major}.{minor} / {version} Version label used in Swagger/ReDoc titles
default_version VersionT | None (1, 0) / "1" Fallback version for routes without @api_version
latest_prefix str | None None If set, adds an alias prefix (e.g. "/latest") pointing to the newest version
include_version_docs bool True Create per-version Swagger UI and ReDoc pages
include_version_openapi_route bool True Create a per-version openapi.json route
include_versions_route bool False Add a GET /versions endpoint listing all active versions
sort_routes bool False Sort routes alphabetically by path within each version
callback Callable[[APIRouter, VersionT, str], None] | None None Hook called once per versioned router, before it is included in the app
webhook_routers APIRouter | list[APIRouter] | None None Router(s) containing webhook definitions annotated with @api_version; each version's schema shows only the webhooks active in that version
openapi_hook Callable[[dict, VersionT], dict] | None None Hook applied to the generated OpenAPI schema for each version; receives (schema, version) and must return the modified schema
swagger_js_url str | None FastAPI CDN Custom URL for the Swagger UI JS bundle
swagger_css_url str | None FastAPI CDN Custom URL for the Swagger UI CSS
swagger_favicon_url str | None FastAPI favicon Custom URL for the Swagger UI favicon
redoc_js_url str | None FastAPI CDN Custom URL for the ReDoc JS bundle
redoc_favicon_url str | None FastAPI favicon Custom URL for the ReDoc favicon
redoc_with_google_fonts bool True If False, ReDoc will not load Google Fonts

Call .versionize() after constructing the object. It returns the list of active versions.


@api_version reference

@api_version(version, *, deprecate_in=None, remove_in=None)
Parameter Type Required Description
version tuple[int, int] | str yes Version when this route is introduced
deprecate_in same | None no Version when this route is marked deprecated in the docs
remove_in same | None no Version from which this route is removed entirely

All three parameters must match the version_format configured on RouterVersioner (tuple[int, int] for SemVer, str for CalVer).


Advanced options

Latest alias

Serve the newest version under a stable prefix that clients can pin to:

RouterVersioner(
    app=app,
    routers=router,
    version_format=VersionFormat.SEMVER,
    latest_prefix="/latest",
).versionize()
# Also mounts /latest/... pointing to the highest version

Version discovery endpoint

RouterVersioner(
    app=app,
    routers=router,
    version_format=VersionFormat.SEMVER,
    include_versions_route=True,
).versionize()
GET /versions
{
  "versions": [
    {
      "version": "1.0",
      "openapi_url": "/v1_0/openapi.json",
      "swagger_url": "/v1_0/docs",
      "redoc_url": "/v1_0/redoc"
    }
  ]
}

Custom URL format

Use prefix_format and semantic_version_format to control how versions appear in URLs and docs.

Major-only versioning (/v1, /v2, /v3):

RouterVersioner(
    app=app,
    routers=router,
    version_format=VersionFormat.SEMVER,
    prefix_format="/v{major}",
    semantic_version_format="{major}",
    latest_prefix="/latest",
).versionize()
# Mounts: GET /v1/items   GET /v2/items   GET /latest/items
# Swagger at /v1/docs, /v2/docs — titles show "v1", "v2"

The route decorator still uses (major, minor) tuples — only the URL and doc label change.

OpenAPI schema hook

openapi_hook lets you modify the generated OpenAPI JSON for each version — useful for custom extensions, logos, version-specific metadata, or AWS API Gateway integration. Unlike overriding app.openapi, this hook is called inside the per-version generation pipeline, so it receives the correct filtered schema.

def my_openapi_hook(schema: dict, version: tuple[int, int]) -> dict:
    # Applied to every version
    schema["info"]["x-logo"] = {"url": "https://example.com/logo.png"}

    # Applied only to v1
    if version == (1, 0):
        schema["info"]["description"] += "\n\n**DEPRECATED:** Use v2."

    return schema

RouterVersioner(
    app=app,
    routers=router,
    version_format=VersionFormat.SEMVER,
    openapi_hook=my_openapi_hook,
).versionize()

The hook receives (schema: dict, version: VersionT) and must return the (modified) dict.

OpenAPI Callbacks and Webhooks

Callbacks (per-route) are propagated automatically — any callbacks=[...] parameter on a route is copied to every versioned copy of that route:

callback_router = APIRouter()

@callback_router.post("{$url}")
def on_event(body: dict) -> None: ...

@router.post("/items", callbacks=callback_router.routes)
@api_version((1, 0))
def create_item() -> dict: ...

Webhooks (app.webhooks) appear in the OpenAPI schema of every version by default. To version webhooks independently, use webhook_routers with the same @api_version decorator used on regular routes:

webhook_router = APIRouter()

@webhook_router.post("/order-created")
@api_version((1, 0))
def webhook_order_v1(body: OrderV1) -> None: ...

@webhook_router.post("/order-created")
@api_version((2, 0))       # replaces v1 definition (same path + method)
def webhook_order_v2(body: OrderV2) -> None: ...

@webhook_router.post("/payment-failed")
@api_version((1, 0), remove_in=(2, 0))
def webhook_payment_v1(body: dict) -> None: ...

RouterVersioner(
    app=app,
    routers=router,
    webhook_routers=webhook_router,
    version_format=VersionFormat.SEMVER,
).versionize()
# /v1_0/openapi.json → webhooks: /order-created (V1), /payment-failed
# /v2_0/openapi.json → webhooks: /order-created (V2)  ← /payment-failed removed

The same introduce / remove_in lifecycle applies. Webhook versions follow route versions: a new webhook version only appears once a route version creates that API prefix.

Multiple routers

Pass a list of routers to version routes that are split across modules:

RouterVersioner(
    app=app,
    routers=[users_router, products_router],
    version_format=VersionFormat.SEMVER,
).versionize()

All routers are versioned together under the same prefix tree.

Self-hosted docs (air-gapped environments)

By default, Swagger UI and ReDoc assets are loaded from the FastAPI CDN. In air-gapped or corporate environments, point them at assets you host yourself:

RouterVersioner(
    app=app,
    routers=router,
    version_format=VersionFormat.SEMVER,
    swagger_js_url="/static/swagger-ui-bundle.js",
    swagger_css_url="/static/swagger-ui.css",
    swagger_favicon_url="/static/favicon.png",
    redoc_js_url="/static/redoc.standalone.js",
    redoc_favicon_url="/static/favicon.png",
    redoc_with_google_fonts=False,
).versionize()

See examples/download_static_assets.py for a ready-made script that downloads all required assets in one step, and examples/self_hosted_docs_app.py for a complete working example.

Reverse proxy / sub-app mounting

When the app runs behind a reverse proxy or is mounted as a sub-application, the ASGI root_path is included in all per-version doc URLs automatically — no extra configuration needed:

parent = FastAPI()
parent.mount("/api", app)  # root_path="/api" is injected at request time
# /api/v1_0/docs correctly references /api/v1_0/openapi.json

Callback hook

Run custom logic each time a versioned router is created — useful for logging or metrics:

def on_version_created(router: APIRouter, version, prefix: str) -> None:
    print(f"Registered version {version} at {prefix}")

RouterVersioner(
    app=app,
    routers=router,
    version_format=VersionFormat.SEMVER,
    callback=on_version_created,
).versionize()

Examples

Runnable examples are available in the examples/ directory:

File What it shows
semver_app.py Full SemVer lifecycle (introduce, deprecate, remove)
calver_app.py Same lifecycle with CalVer date strings
semver_major_only_app.py Custom prefix /v1, /v2 via prefix_format
webhook_versioning_app.py Per-version webhook definitions via webhook_routers
multi_router_app.py Multiple routers versioned together
self_hosted_docs_app.py Swagger UI and ReDoc from local static assets

License

MIT

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

fastapi_router_versioning-0.1.1.tar.gz (91.0 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

fastapi_router_versioning-0.1.1-py3-none-any.whl (13.8 kB view details)

Uploaded Python 3

File details

Details for the file fastapi_router_versioning-0.1.1.tar.gz.

File metadata

  • Download URL: fastapi_router_versioning-0.1.1.tar.gz
  • Upload date:
  • Size: 91.0 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: uv/0.11.18 {"installer":{"name":"uv","version":"0.11.18","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

File hashes

Hashes for fastapi_router_versioning-0.1.1.tar.gz
Algorithm Hash digest
SHA256 83b508c7fc5f4602ad74388b16730f7f82bf8e675ad3e11cfc57e657aab32e84
MD5 bf487c3e47a8f5795a6d6e88ba469624
BLAKE2b-256 24883d3911ea829d78ebbaa52a8225120e59bcea988896bff290f46959f5ac6d

See more details on using hashes here.

File details

Details for the file fastapi_router_versioning-0.1.1-py3-none-any.whl.

File metadata

  • Download URL: fastapi_router_versioning-0.1.1-py3-none-any.whl
  • Upload date:
  • Size: 13.8 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: uv/0.11.18 {"installer":{"name":"uv","version":"0.11.18","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

File hashes

Hashes for fastapi_router_versioning-0.1.1-py3-none-any.whl
Algorithm Hash digest
SHA256 d85166fa34e1a89b48fd7ef8d33eb3bcf2e9da12400b64c183858eab5d7d0523
MD5 ff3cdbc8da3d1a23c5a87a9bb46bbd51
BLAKE2b-256 99ccc1c45c3d9e575ab68f58a9806e10cfccdb2729f4bf79f94f351eeaadf16b

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page