FastAPI’s APIRouter is a powerful tool for organizing your application, but its true potential for clarity and maintainability is unlocked when you leverage prefixing and tagging.

Let’s see it in action. Imagine a simple FastAPI app with two distinct sets of endpoints: one for user management and another for product catalog.

# main.py
from fastapi import FastAPI
from routers import users, products

app = FastAPI()

app.include_router(users.router, prefix="/users", tags=["Users"])
app.include_router(products.router, prefix="/products", tags=["Products"])

if __name__ == "__main__":
    import uvicorn
    uvicorn.run(app, host="0.0.0.0", port=8000)

Here, routers/users.py and routers/products.py would each contain their own APIRouter instances:

# routers/users.py
from fastapi import APIRouter

router = APIRouter()

@router.get("/me")
async def read_users_me():
    return {"message": "Current user details"}

@router.get("/{user_id}")
async def read_user(user_id: int):
    return {"user_id": user_id}
# routers/products.py
from fastapi import APIRouter

router = APIRouter()

@router.get("/")
async def list_products():
    return {"products": ["Laptop", "Keyboard", "Mouse"]}

@router.get("/{product_id}")
async def get_product(product_id: str):
    return {"product_id": product_id}

When you run this application and visit /docs or /redoc, you’ll see the endpoints neatly grouped. The prefix="/users" in app.include_router means all routes defined within routers.users.router will automatically be prepended with /users. So, @router.get("/me") becomes /users/me, and @router.get("/{user_id}") becomes /users/{user_id}. Similarly, product routes will be found under /products.

The tags=["Users"] and tags=["Products"] arguments are crucial for the interactive API documentation. They don’t affect routing or behavior but organize the endpoints in the Swagger UI and ReDoc interfaces into collapsible sections. This makes navigating large APIs infinitely easier, allowing users to quickly find related operations.

Under the hood, FastAPI processes these APIRouter instances during the include_router call. It takes the prefix and applies it to every path defined within the router. If a router defines a path like /items, and you include it with prefix="/api/v1", the effective path becomes /api/v1/items. The tags are associated with each route operation within that router and are then used by the documentation generation tools.

This separation allows you to manage related endpoints in their own files, promoting modularity and making your codebase much more readable. Instead of one giant FastAPI app instance with dozens of route definitions, you have smaller, focused APIRouter objects that are then composed into the main application. This is analogous to how microservices break down functionality, but applied within a single monolithic application for better organization.

The power of APIRouter extends to dependency injection as well. You can define dependencies at the router level using router.dependencies, and these will be applied to all routes within that router. This means you can enforce authentication or data validation for an entire group of endpoints without repeating the dependency declaration on each individual route. For example, you could add dependencies=[Depends(verify_api_key)] to a router, and every endpoint within that router would require a valid API key.

When you define a route directly on the FastAPI app instance, e.g., @app.get("/"), that route lives at the root of your application. However, when you include an APIRouter with a prefix, like app.include_router(my_router, prefix="/v1"), the routes defined within my_router are nested under that prefix. If my_router has a route @my_router.get("/users"), it will be accessible at /v1/users, not at the root level. This hierarchical structure is fundamental to building organized APIs, allowing for versioning or logical grouping of functionalities.

The most surprising thing about APIRouter is how seamlessly it integrates into the main FastAPI app, allowing you to treat a collection of routes as a single unit. You can even include routers within other routers, creating a nested structure that mirrors complex application architectures.

The next logical step in organizing your FastAPI application is understanding how to handle global exception handlers across your routers.

Want structured learning?

Take the full Fastapi course →