APIRouter Prefix vs Sub-Application Mounting in FastAPI
Key takeaways:
include_routercomposes into one app: shared schema, middleware, and handlers.mountattaches an independent app: its own schema, middleware, and lifespan.- Default to
include_routerfor a cohesive API. - Mount only when a section needs genuine isolation.
- A mounted app inherits nothing, so re-add cross-cutting concerns to it.
This decision guide supports Modular Router Organization.
The Problem This Solves
Both include_router and mount put routes under a path prefix, so they look interchangeable, but they differ on everything that matters operationally: documentation, middleware, and lifecycle. Choosing wrong means either a fractured schema or a section that cannot be isolated when it needs to be.
The Comparison
| Aspect | include_router | mount (sub-application) |
|---|---|---|
| OpenAPI schema | Shared, one document | Separate per app |
| Middleware | Inherited from parent | Independent |
| Exception handlers | Inherited | Independent |
| Lifespan | Shared | Independent |
| Prefix behavior | Composes | Mounts at a path |
| Best for | One cohesive API | Isolated or foreign sections |
Step-by-Step: Choosing
1. Cohesive API → include_router
from fastapi import FastAPI
from app.routers import v1 # Composed domain routers under /v1.
def create_app() -> FastAPI:
app = FastAPI()
app.include_router(v1) # Shares schema, middleware, handlers.
return app
2. Isolated section → mount
from fastapi import FastAPI
# A separate app with its own middleware and docs.
internal = FastAPI(title="Internal Admin")
def create_app() -> FastAPI:
app = FastAPI()
app.mount("/internal", internal) # Independent schema and lifecycle.
return app
Edge Cases and Gotchas
- Lost middleware. A mounted app does not get the parent's tracing or error envelope; configure them on it, per Middleware Implementation and Error Handling and Global Exceptions.
- Duplicate docs. Mounting creates a second
/docs; communicate which is canonical. - Lifespan scope. A mounted app's startup runs independently; do not assume shared resources.
Verification
def test_routing_and_isolation(client):
assert client.get("/v1/users/1").status_code == 200 # Included router.
assert client.get("/internal/openapi.json").status_code == 200 # Own schema.
Related Reading
- Up to the topic: Modular Router Organization.
- Related guides: Structuring Large FastAPI Projects for Scale and Application Factory Patterns.