APIRouter Prefix vs Sub-Application Mounting in FastAPI

Key takeaways:

  • include_router composes into one app: shared schema, middleware, and handlers.
  • mount attaches an independent app: its own schema, middleware, and lifespan.
  • Default to include_router for 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

Aspectinclude_routermount (sub-application)
OpenAPI schemaShared, one documentSeparate per app
MiddlewareInherited from parentIndependent
Exception handlersInheritedIndependent
LifespanSharedIndependent
Prefix behaviorComposesMounts at a path
Best forOne cohesive APIIsolated 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.