Search Topics
Search across all FastAPI topics
APIRouter
Core ConceptAPIRouter 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.
# 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
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
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.
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
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.
# 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 reachedThink 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