Skip to main content

Structuring Applications with APIRouter

In large applications, keeping all path operations in a single file becomes difficult to maintain. The APIRouter class allows you to group related routes into separate modules, which can then be integrated into your main application.

By the end of this tutorial, you will have a structured application with routes organized into a dedicated module, complete with shared prefixes, tags, and dependencies.

Prerequisites

To follow this tutorial, you should have a basic FastAPI application structure. This codebase uses Pydantic for data validation and Starlette for routing.

Step 1: Create a Router Module

First, create a new file (e.g., routers/items.py) to house a specific set of related endpoints. Instead of using the FastAPI class, you will use APIRouter.

from fastapi import APIRouter, Depends, HTTPException
from ..dependencies import get_token_header

router = APIRouter(
prefix="/items",
tags=["items"],
dependencies=[Depends(get_token_header)],
responses={404: {"description": "Not found"}},
)

In this step, you initialize the APIRouter. The parameters you pass here apply to every route defined on this router:

  • prefix: Prepends /items to all paths. Note that the prefix must start with / and must not end with /, as enforced by the APIRouter constructor.
  • tags: Adds the "items" tag to all operations for OpenAPI documentation.
  • dependencies: Executes the get_token_header dependency for every request to these routes.
  • responses: Defines a default 404 response for all routes in this group.

Step 2: Define Path Operations

Now, add path operations to your router using decorators like @router.get() and @router.put().

fake_items_db = {"plumbus": {"name": "Plumbus"}, "gun": {"name": "Portal Gun"}}

@router.get("/")
async def read_items():
return fake_items_db

@router.get("/{item_id}")
async def read_item(item_id: str):
if item_id not in fake_items_db:
raise HTTPException(status_code=404, detail="Item not found")
return {"name": fake_items_db[item_id]["name"], "item_id": item_id}

@router.put(
"/{item_id}",
tags=["custom"],
responses={403: {"description": "Operation forbidden"}},
)
async def update_item(item_id: str):
if item_id != "plumbus":
raise HTTPException(
status_code=403, detail="You can only update the item: plumbus"
)
return {"item_id": item_id, "name": "The great Plumbus"}

Even though the @router.get("/") path looks like the root, it will actually be served at /items/ because of the prefix defined in Step 1. For the update_item operation, the tags and responses you provide are merged with the router's defaults.

Step 3: Include the Router in the Main App

To make these routes active, you must include the router in your main FastAPI application file (e.g., main.py).

from fastapi import Depends, FastAPI
from .routers import items, users
from .dependencies import get_query_token

app = FastAPI(dependencies=[Depends(get_query_token)])

# Include the router defined in Step 1 & 2
app.include_router(items.router)

@app.get("/")
async def root():
return {"message": "Hello Bigger Applications!"}

The app.include_router() method registers all the routes from items.router into the main application.

Step 4: Override Configuration During Inclusion

You can also apply or override configurations at the moment you include a router. This is useful for reusing the same router in different contexts.

from .internal import admin
from .dependencies import get_token_header

app.include_router(
admin.router,
prefix="/admin",
tags=["admin"],
dependencies=[Depends(get_token_header)],
responses={418: {"description": "I'm a teapot"}},
)

When you provide parameters to include_router, FastAPI handles them as follows:

  • Prefixes: The new prefix is prepended to any prefix already defined on the router.
  • Tags and Dependencies: These are appended to the existing lists.
  • Responses: These are merged with existing responses.

Step 5: Nesting Routers

APIRouter also supports nesting. You can include one router inside another before finally including the parent router in the FastAPI app.

# Inside a hypothetical internal_router.py
from fastapi import APIRouter
from .routers import users_router

internal_router = APIRouter(prefix="/internal")
internal_router.include_router(users_router, prefix="/users")

# Resulting paths will be /internal/users/...

Summary

You have successfully organized your application by:

  1. Creating an APIRouter instance in a separate module.
  2. Configuring shared prefixes and dependencies at the router level.
  3. Integrating the router into the main FastAPI application using include_router.

This structure keeps your main.py clean and allows teams to work on different modules independently. For next steps, explore adding custom lifespan handlers to your routers to manage startup and shutdown logic for specific modules.