DEV Community

Cover image for Mastering Routing in FastAPI: From Flat Files to Clean Architecture
Arun Yadav
Arun Yadav

Posted on

Mastering Routing in FastAPI: From Flat Files to Clean Architecture

When building an API with FastAPI, it’s deceptively easy to dump every endpoint into main.py. A few @app.get() decorators here, a couple of @app.post() decorators there, and everything works.

Then your project grows. Suddenly, main.py is an 800-line monolith, team members are constantly hitting Git merge conflicts, and nobody knows where the user authentication logic ends and the billing logic begins.

FastAPI solves this with APIRouter. Here is how to structure your routing cleanly from day one.


1. The Core Tool: APIRouter

Think of APIRouter as a mini FastAPI app that doesn’t run on its own. It groups related path operations together, which you then mount onto your main application.

A typical modular directory structure looks like this:

my_project/
├── app/
│   ├── routers/
│   │   ├── users.py
│   │   └── items.py
│   ├── dependencies.py
│   └── main.py
└── requirements.txt

Enter fullscreen mode Exit fullscreen mode

In your module (app/routers/users.py), define your router:

from fastapi import APIRouter, HTTPException

router = APIRouter()

@router.get("/")
async def list_users():
    return [{"id": 1, "username": "alice"}]

@router.get("/{user_id}")
async def get_user(user_id: int):
    return {"id": user_id, "username": "alice"}

Enter fullscreen mode Exit fullscreen mode

Then, register it in app/main.py:

from fastapi import FastAPI
from app.routers import users

app = FastAPI()

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

Enter fullscreen mode Exit fullscreen mode

Two key parameters to notice:

  • prefix: Prepend /users to every endpoint in the router. Notice how users.py uses / and /{user_id} instead of repeating /users everywhere.
  • tags: Groups these endpoints together in FastAPI's auto-generated Swagger UI (/docs).

2. Cleaner Endpoints with Shared Dependencies

One of the biggest advantages of APIRouter is attaching security checks or common dependencies to an entire group of routes at once.

Instead of writing Depends(verify_token) on 15 separate endpoints, apply it to the router level:

# app/routers/admin.py
from fastapi import APIRouter, Depends
from app.dependencies import require_admin_role

# Every route attached to this router will now require admin access
router = APIRouter(
    prefix="/admin",
    tags=["Admin"],
    dependencies=[Depends(require_admin_role)]
)

@router.delete("/purge-cache")
async def purge_cache():
    return {"status": "cache cleared"}

Enter fullscreen mode Exit fullscreen mode

3. Nesting Routers for Sub-Resources

Real-world APIs often have hierarchical relationships—such as comments belonging to a specific post (/posts/{post_id}/comments).

Rather than hardcoding long prefix strings, routers can be nested inside other routers:

# app/routers/comments.py
from fastapi import APIRouter

comments_router = APIRouter()

@comments_router.get("/")
async def get_comments(post_id: int):
    return {"post_id": post_id, "comments": []}

Enter fullscreen mode Exit fullscreen mode
# app/routers/posts.py
from fastapi import APIRouter
from app.routers.comments import comments_router

posts_router = APIRouter(prefix="/posts", tags=["Posts"])

# Mount comments directly under a specific post
posts_router.include_router(
    comments_router,
    prefix="/{post_id}/comments",
    tags=["Comments"]
)

Enter fullscreen mode Exit fullscreen mode

Now, mounting posts_router inside your main.py automatically exposes both /posts and /posts/{post_id}/comments cleanly.


4. API Versioning Without Headaches

Versioning is simple when your routing is modular. You can bundle whole sets of routers under a version prefix:

# app/main.py
from fastapi import FastAPI, APIRouter
from app.routers import users, items

app = FastAPI()

# Create a v1 router
api_v1 = APIRouter(prefix="/api/v1")
api_v1.include_router(users.router, prefix="/users", tags=["Users"])
api_v1.include_router(items.router, prefix="/items", tags=["Items"])

# Include the full v1 bundle into the main app
app.include_router(api_v1)

Enter fullscreen mode Exit fullscreen mode

When it's time for v2, you can spin up an api_v2 router alongside it without touching older client integrations.


Top comments (0)