As an API evolves you need versioning (/api/v1 → /api/v2) and sometimes genuinely separate applications (admin panel, public API, internal tools) inside one process. FastAPI offers include_router with prefixes for route grouping and app.mount() for full sub-applications.
from fastapi import FastAPI, APIRouter
v1 = APIRouter(prefix="/api/v1", tags=["v1"])
v2 = APIRouter(prefix="/api/v2", tags=["v2"])
@v1.get("/users/{user_id}")
def get_user_v1(user_id: int):
return {"id": user_id} # old shape
@v2.get("/users/{user_id}")
def get_user_v2(user_id: int):
return {"data": {"id": user_id}, "meta": {}} # new shape
app = FastAPI(title="My API", version="2.0.0")
app.include_router(v1)
app.include_router(v2)
deprecated=True on routers/endpoints; the docs show them grayed out.admin_app = FastAPI(docs_url="/admin/docs", title="Admin")
@admin_app.get("/stats/")
def stats():
return {"users": 4211}
app = FastAPI(title="Main")
app.mount("/admin", admin_app) # everything under /admin/* goes to admin_app
app.mount("/static", StaticFiles(directory="static"), name="static")
/admin/docs), OpenAPI schema, middleware and exception handlers.mount takes the first matching prefix: mount specific paths (/admin, /static) before anything generic like a catch-all /{path:path}.get_db) rather than globals on the parent app.| Need | Use |
|---|---|
| New response shapes for the same resources | include_router with /api/vN prefix |
| Different auth or middleware for a section | include_router(dependencies=[...]) or a mounted app |
| Own docs/OpenAPI for a section (admin, partner API) | app.mount() |
| Serving static assets / SPAs / other frameworks (Flask app, Dash) | app.mount() |
from fastapi.staticfiles import StaticFiles
from fastapi.responses import FileResponse
app.mount("/assets", StaticFiles(directory="frontend/dist/assets"), name="assets")
@app.get("/{full_path:path}", include_in_schema=False) # SPA fallback - keep LAST
def spa(full_path: str):
return FileResponse("frontend/dist/index.html")
Mount assets first, then a catch-all returning index.html so client-side routing works on refresh. Declare it include_in_schema=False and register it after every real route.
/api/v1/... — explicit, cacheable, easy to route and to sunset.Accept: application/vnd.myapi.v2+json — cleaner URLs but harder to test/cache; implement via a dependency reading headers.A: Two usual causes: another route (often a catch-all or a similar prefix declared earlier) matched first, or the client called the wrong full path — mounted paths are absolute: admin_app's /stats/ is served at /admin/stats/, not /stats/.
A: No. A sub-application runs its own middleware stack and its own (optional) lifespan — that isolation is often the reason to mount. If you need shared behavior, extract it into modules used by both apps or attach it to each app.
A: Ideally two: current and previous (previous read-only or deprecated with a sunset date). Every parallel version doubles testing and security surface — publish a deprecation policy and let the docs' deprecated=True flags communicate it.
Version with APIRouter prefixes by default and reserve app.mount() for sections that deserve their own app identity — separate docs, auth or static/SPA serving. Together with the routing structure from Basic, this scales an API from one file to a multi-version platform.