Sub-Applications, Mounting and API Versioning

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.

include_router: The Everyday Versioning Tool

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)

app.mount(): Full Sub-Applications

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")

Which One When?

NeedUse
New response shapes for the same resourcesinclude_router with /api/vN prefix
Different auth or middleware for a sectioninclude_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()

Serving an SPA (React/Angular Build) from FastAPI

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.

Versioning Strategies Beyond URL Prefixes

Quick Questions and Answers

Q: My mounted sub-app's routes return 404 — why?

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/.

Q: Do mounted apps share middleware and lifespan with the parent?

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.

Q: How many versions should I keep alive?

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.

Conclusion

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.