APIRouter — Organising Routes into Modules

As a FastAPI application grows, putting all routes in a single main.py file becomes unmanageable. APIRouter is FastAPI’s solution — it lets you define a group of routes in a separate module, then include that group in the main application with a URL prefix and tags. The result is a cleanly organised codebase where each resource (posts, users, comments, auth) lives in its own file with its own router. Route handlers in a router file look identical to those in main.py; the only difference is they use router instead of app.

Creating and Using APIRouter

# app/routers/posts.py
from fastapi import APIRouter, Depends, HTTPException, status
from app.schemas.post import PostCreate, PostUpdate, PostResponse
from app.dependencies import get_db, get_current_user

# Create a router with a prefix and tags
router = APIRouter(
    prefix   = "/posts",          # all routes get /posts prefix
    tags     = ["Posts"],         # group in Swagger UI
    responses = {                 # shared response docs for all routes
        404: {"description": "Post not found"},
    },
)

@router.get("/", response_model=list[PostResponse])
def list_posts(page: int = 1, limit: int = 10, db = Depends(get_db)):
    offset = (page - 1) * limit
    return db.query(Post).offset(offset).limit(limit).all()

@router.get("/{post_id}", response_model=PostResponse)
def get_post(post_id: int, db = Depends(get_db)):
    post = db.query(Post).get(post_id)
    if not post:
        raise HTTPException(status.HTTP_404_NOT_FOUND, "Post not found")
    return post

@router.post("/", response_model=PostResponse, status_code=201)
def create_post(
    post: PostCreate,
    db = Depends(get_db),
    current_user = Depends(get_current_user),
):
    db_post = Post(**post.model_dump(), author_id=current_user.id)
    db.add(db_post)
    db.commit()
    db.refresh(db_post)
    return db_post
Note: When you include a router with app.include_router(router, prefix="/posts"), and the router itself also has prefix="/posts", the prefixes stack — routes end up at /posts/posts/.... Avoid this by either setting the prefix in the router definition OR in include_router(), not both. The convention is to set the prefix in include_router() in main.py and leave the router’s own prefix empty, which makes it clear from main.py what URL each router is mounted at.
Tip: Use router-level dependencies to apply a dependency to every route in the router without repeating it. router = APIRouter(dependencies=[Depends(require_auth)]) means every endpoint in that router requires authentication — you do not need to add Depends(require_auth) to each individual route. This is the cleanest pattern for applying authentication to a whole group of routes while keeping a few public routes in a separate, unauthenticated router.
Warning: Do not create circular imports by having routers import from each other. If posts.py needs a schema from users.py, import the schema directly, not the router. A clean dependency graph: main.py imports routers, routers import schemas and dependencies, dependencies import models, models import the database base. Never have a router import another router.

Including Routers in main.py

# app/main.py
from fastapi import FastAPI
from app.routers import posts, users, auth, comments

app = FastAPI(title="Blog API", version="1.0.0")

# Include routers — prefix and tags set here (not in router files)
app.include_router(auth.router,     prefix="/auth",     tags=["Auth"])
app.include_router(users.router,    prefix="/users",    tags=["Users"])
app.include_router(posts.router,    prefix="/posts",    tags=["Posts"])
app.include_router(comments.router, prefix="/comments", tags=["Comments"])

# Result:
# /auth/login       (from auth router)
# /auth/logout      (from auth router)
# /users            (from users router)
# /users/{user_id}  (from users router)
# /posts            (from posts router)
# /posts/{post_id}  (from posts router)

Router-Level Dependencies

from fastapi import APIRouter, Depends
from app.dependencies import require_auth, require_admin

# Public router — no auth required
public_router = APIRouter(prefix="/posts", tags=["Posts (Public)"])

@public_router.get("/")
def list_public_posts(): ...

@public_router.get("/{id}")
def get_public_post(id: int): ...

# Protected router — auth required for ALL routes
protected_router = APIRouter(
    prefix       = "/posts",
    tags         = ["Posts (Auth)"],
    dependencies = [Depends(require_auth)],  # applied to every route below
)

@protected_router.post("/")            # automatically requires auth
def create_post(): ...

@protected_router.put("/{id}")         # automatically requires auth
def update_post(id: int): ...

@protected_router.delete("/{id}")      # automatically requires auth
def delete_post(id: int): ...

# Admin router — admin role required
admin_router = APIRouter(
    prefix       = "/admin",
    tags         = ["Admin"],
    dependencies = [Depends(require_admin)],
)

@admin_router.get("/posts")            # requires admin role
def admin_list_all_posts(): ...

# In main.py: include all three routers
# app.include_router(public_router)
# app.include_router(protected_router)
# app.include_router(admin_router)

Common Mistakes

Mistake 1 — Double prefix from both router and include_router

❌ Wrong — prefix set in both places:

router = APIRouter(prefix="/posts")       # prefix here
app.include_router(router, prefix="/posts")  # AND here → /posts/posts/ !

✅ Correct — prefix in one place only:

router = APIRouter()                          # no prefix
app.include_router(router, prefix="/posts")  # prefix only here ✓

Mistake 2 — Circular router imports

❌ Wrong — routers importing each other:

# posts.py
from app.routers.users import get_user   # circular import risk

✅ Correct — import schemas/services, not routers:

# posts.py
from app.schemas.user import UserResponse   # ✓ import schema, not router

Mistake 3 — One massive router file (defeats the purpose)

❌ Wrong — all 50 routes in one router:

# routers/all_routes.py — 1000 lines, all resources mixed together

✅ Correct — one router per resource:

# routers/posts.py, routers/users.py, routers/comments.py, etc.   ✓

Quick Reference

Task Code
Create router router = APIRouter()
Router with tags APIRouter(tags=["Posts"])
Add route to router @router.get("/") def f(): ...
Include in app app.include_router(router, prefix="/posts")
Router-level deps APIRouter(dependencies=[Depends(require_auth)])
Shared responses APIRouter(responses={404: {"description": "..."}})

🧠 Test Yourself

You have a router with 10 routes that all require authentication. What is the cleanest way to apply the require_auth dependency to all of them?




(also works, but sets it for the included router globally)