Modular Router Organization in FastAPI
Modular router organization is the practice of splitting endpoints into domain-focused APIRouter modules, each with its own prefix and tags, then composing them in the application factory rather than declaring every route on one object.
This is the routing half of Core Architecture and Routing Patterns, and it pairs tightly with the application factory, which is where routers are assembled, and with dependency injection, since router-level dependencies gate whole groups of routes. Good router structure is what keeps a large API navigable.
Core Mechanics: Composing Routers
An APIRouter is a mountable collection of routes. You include it into an app or into another router with include_router, and prefixes compose, so a /users router included into a /v1 router yields /v1/users. Tags carry through to the OpenAPI document, grouping endpoints in the docs the way you grouped them in code.
# app/routers/users.py
from fastapi import APIRouter
router = APIRouter(prefix="/users", tags=["users"])
@router.get("/{user_id}")
async def get_user(user_id: int) -> dict[str, int]:
return {"user_id": user_id}
# app/routers/__init__.py — compose domain routers under one version.
from fastapi import APIRouter
from app.routers import users, orders, billing
v1 = APIRouter(prefix="/v1")
v1.include_router(users.router)
v1.include_router(orders.router)
v1.include_router(billing.router)
Production Implementation: Versioning and Sub-Applications
For most APIs, version by including each version's routers under a prefix in the factory. When a section needs true isolation — different middleware, an independent OpenAPI document, or its own lifespan — mount it as a sub-application instead.
# app/main.py
from fastapi import FastAPI
from app.routers import v1
from app.internal import internal_app # A separate FastAPI() instance.
def create_app() -> FastAPI:
app = FastAPI()
app.include_router(v1) # Shares this app's schema and middleware.
app.mount("/internal", internal_app) # Isolated: own docs, own middleware stack.
return app
The full project-structure walk-through, from single file to domain packages, is in Structuring Large FastAPI Projects for Scale.
Async and Performance Notes
Router composition is resolved once at startup; it has no per-request cost beyond normal route matching. The performance-relevant decisions are elsewhere: keep response models centralized so OpenAPI generation stays fast, and attach router-level dependencies thoughtfully, since a dependency on a router runs for every route in it on every request.
Testing Strategy
Test routers in isolation by including a single router into a minimal app, which keeps the surface under test small:
from fastapi import FastAPI
from fastapi.testclient import TestClient
from app.routers import users
def test_users_router_alone():
app = FastAPI()
app.include_router(users.router, prefix="/v1")
client = TestClient(app)
assert client.get("/v1/users/1").status_code == 200
Failure Modes and Debugging
- Duplicate path operations. Importing a router twice or re-including it registers routes twice; include each router exactly once, in the factory.
- Prefix drift. Hard-coding
/v1inside individual routes instead of composing prefixes makes versioning a find-and-replace. Let prefixes compose. - Circular imports. Routers importing services that import routers stall startup; wire shared objects through the factory, as in Dependency Injection Strategies.
- Sub-app surprises. A mounted sub-application does not inherit the parent's middleware or exception handlers; configure them on the sub-app too.
Related Reading
- Up to the section: Core Architecture and Routing Patterns.
- Hands-on guide: Structuring Large FastAPI Projects for Scale.
- Composes with: Application Factory Patterns and Dependency Injection Strategies.