Search Topics

Search across all FastAPI topics

GitHub

APIRouter

Core Concept

APIRouter lets you split your endpoints into separate modules. Think of it as a mini FastAPI app that gets mounted on the main app.

Your main.py has 47 endpoints. You need to add rate limiting to all admin routes. That means finding every admin endpoint scattered across 1500 lines and wrapping each one individually. There has to be a better way.

terminal
# main.py — 1,500 lines and growing...

@app.get("/admin/users")      # line 234
@app.get("/admin/settings")   # line 567
@app.post("/admin/ban")       # line 891
@app.get("/items")            # line 1023
@app.post("/items")           # line 1156
# ... 42 more endpoints ...

# "Wait, which ones are admin routes again?"

Question

What if you could group all your admin routes in one file, slap a single prefix and auth dependency on them, and include them in your main app with one line? That's exactly what APIRouter does.

The Big Picture

Instead of one massive file, you split your API into focused modules. Each module owns its routes, its prefix, and its dependencies. Then your main app just assembles the pieces.

items.py

/items/*

users.py

/users/*

admin.py

/admin/*

main.py

Assembles all routers

The Problem

What you just learned

A single file with dozens of endpoints becomes impossible to navigate

Applying shared logic (auth, rate limiting) requires touching every endpoint

APIRouter lets you group related endpoints into separate modules

Creating a Router

Here's the key insight: a router works exactly like a mini FastAPI app. You define endpoints on it the same way you would on app — with decorators like @router.get().

routers/items.py
# routers/items.py
from fastapi import APIRouter

# Create a router — it's like a mini FastAPI app
router = APIRouter(
    prefix="/items",    # All routes here start with /items
    tags=["items"],     # Groups them in the docs
)

@router.get("/")
async def list_items():
    # This becomes GET /items/
    return [{"name": "Foo"}, {"name": "Bar"}]

@router.get("/{item_id}")
async def get_item(item_id: int):
    # This becomes GET /items/{item_id}
    return {"item_id": item_id}

Insight

Notice how the router defines prefix="/items" once, and every route automatically gets that prefix. No more repeating /items in every decorator.

Including Routers

Your main.py stays clean. It just mounts the routers — one line each.

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

app = FastAPI()

# One line per module — that's it
app.include_router(items.router)
app.include_router(users.router)

# items endpoints: /items/, /items/{item_id}
# users endpoints: /users/, /users/{user_id}

Router Basics

What you just learned

APIRouter works just like a mini FastAPI app — same decorators, same patterns

prefix sets the URL prefix for all routes in the router

include_router() mounts a router onto your main app with one line

See It Assemble

Watch routers mount onto the app and see how prefixes resolve into final URLs.

Router-Level Dependencies

Remember that 1,500-line nightmare from the top of this page? Here's how you'd actually solve it. Instead of adding Depends(verify_admin_token) to every single admin endpoint, you set it once on the router.

routers/admin.py
from fastapi import APIRouter, Depends

# Every endpoint in this router gets auth automatically
router = APIRouter(
    prefix="/admin",
    tags=["admin"],
    dependencies=[Depends(verify_admin_token)],  # Applied to ALL routes
)

@router.get("/stats")
async def admin_stats():
    # verify_admin_token runs automatically — you didn't have to add it
    return {"users": 100}

@router.get("/settings")
async def admin_settings():
    # Same here — auth is handled by the router
    return {"theme": "dark"}

@router.post("/ban/{user_id}")
async def ban_user(user_id: int):
    # And here too. One dependency, every route protected.
    return {"banned": user_id}

Go Deeper: Nested Routers

Routers can include other routers. This lets you build a hierarchy — your main app includes a v1 router, which includes items and users routers underneath it.

routers/v1/__init__.py
# routers/v1/__init__.py
from fastapi import APIRouter
from .items import router as items_router
from .users import router as users_router

router = APIRouter(prefix="/v1")
router.include_router(items_router)   # /v1/items/...
router.include_router(users_router)   # /v1/users/...

# main.py
app.include_router(v1_router)
# All routes now start with /v1/

Watch out

Don't go overboard with nesting. Two levels deep (like /v1/items) is usually the sweet spot. Three or more levels and you'll start losing track of where routes actually resolve to.

Advanced Routing

What you just learned

Router-level dependencies apply auth/logic to ALL routes in the router

Routers can include other routers for hierarchical URL structures

This pattern makes security opt-out instead of opt-in — much safer

Silent Route Shadowing

You add a new router for v2 items but forget to give it a unique prefix. Your v2 endpoints never get called — they're silently shadowed by v1.

Broken code
main.py
# routers/items_v1.py
router = APIRouter(tags=["items-v1"])

@router.get("/items")
async def list_items_v1():
    return {"version": 1, "items": [...]}

# routers/items_v2.py
router = APIRouter(tags=["items-v2"])

@router.get("/items")  # Same path as v1!
async def list_items_v2():
    return {"version": 2, "items": [...]}

# main.py
app.include_router(items_v1.router)  # Registered first
app.include_router(items_v2.router)  # Registered second — never reached

Think about it...

If two routers both define @router.get('/items') and you include both without prefixes, what happens?

Hint: Think about the order you call app.include_router().

Key Points

Modular Organization

Split endpoints into separate files by domain

Prefix & Tags

Set URL prefix and OpenAPI tags at the router level

Shared Dependencies

Apply auth and other checks to all routes in a router

Nested Routers

Routers can include other routers for deep nesting