Skip to content

Refactoring with APIRouter

Putting all our endpoints in main.py works fine for a small project. However, as our application grows to dozens or hundreds of endpoints, main.py becomes hard to navigate and maintain.

FastAPI provides a tool called APIRouter to help us split our application into smaller, focused modules. An APIRouter is essentially a "mini-FastAPI" app that we can merge into the main application.

The routers package

projects.py will live in a new package in our code called routers, so we'll make a new folder with an empty __init__.py.


Creating a Router

First, we create a new file for our router. A common convention is to group related endpoints into a routers directory.

Here's a router for the project endpoints:

src/release_tracker/routers/projects.py
from typing import Any

from fastapi import APIRouter

from .. import crud
from ..dependencies import ProjectDep, SessionDep
from ..models import ProjectRead

router = APIRouter(prefix="/projects", tags=["projects"])


@router.get("/", response_model=list[ProjectRead])
def list_projects(session: SessionDep) -> Any:
    return crud.list_projects(session)


@router.get("/{project_id}", response_model=ProjectRead)
def get_project(project: ProjectDep) -> Any:
    return project

SessionDep and ProjectDep carry over from dependencies.py. The lookup-or-404 still happens, but inside get_project_or_404, not the route. The get_project handler is a one-liner that returns the injected model.

Info

The -> Any return type tells static type checkers not to constrain the return value.

FastAPI uses response_model to validate, serialize, and document the response, so we don't need to provide a return type hint. Using Any keeps the type checker happy.

Returning the SQLModel Project instance is fine; FastAPI converts it into a ProjectRead for the response.

Differences from FastAPI

  1. Instantiation:

    Create an APIRouter() instead of FastAPI().

  2. Decorator:

    Use @router.get() instead of @app.get().

  3. Prefix:

    By passing prefix="/projects" when we create the router, we don't have to repeat it in every route handler. Our @router.get("/") actually maps to /projects/, and @router.get("/{project_id}") maps to /projects/{project_id}.


  1. Tags:

    Optionally assign tags=["projects"] to the router, grouping all of its endpoints together in the interactive OpenAPI documentation.

Integrating the Router

Once our router is defined, we need to tell our main application about it. We do this using the app.include_router() method.

Here's how our main.py is transformed:

src/release_tracker/main.py
# ... imports ...
from .routers import projects

app = FastAPI(...)

# Include our new router
app.include_router(projects.router)

# ... middleware and exception handlers ...

The project endpoints now live in their own module; main.py includes them with app.include_router(projects.router).