Lesson 9 · FastAPI deep dive
FastAPI Tutorial #9: Organising Routes with APIRouter
Video nine of the eighteen-part series: splitting one giant app across organized, reusable pieces. APIRouter, includerouter, shared prefixes, tags, and…
- CourseFastAPI deep dive
- Lesson9 of 12
- Video16 min
- FormatJupyter notebook · 10 code cells
What you'll learn
- The Problem - One Giant File
- APIRouter - a Mini FastAPI App
- includerouter - Wiring It Into the App
- prefix - Avoiding Repeated Path Segments
- tags on the Router - Applying to Every Route at Once
- Router-Level Dependencies
- Multiple Independent Routers in One App
- Nested Routers - Including One Router Inside Another
Data
No separate download needed — the notebook creates or downloads everything it uses.
📓 Full notebook
Download .ipynbFastAPI Deep-Dive, Video 9: APIRouter - Organizing Routes#
- Video nine of the eighteen-part series: splitting one giant app across organized, reusable pieces.
- APIRouter, include_router, shared prefixes, tags, and router-level dependencies.
- Let's get into it.
Part 1: The Problem - One Giant File#
from fastapi import FastAPI
from fastapi.testclient import TestClient
app = FastAPI()
@app.get('/users')
def list_users():
return [{'id': 1, 'name': 'Alex'}]
@app.get('/products')
def list_products():
return [{'id': 1, 'name': 'Mug'}]
client = TestClient(app)
print(client.get('/users').json())
Part 2: APIRouter - a Mini FastAPI App#
from fastapi import APIRouter
users_router = APIRouter()
@users_router.get('/users')
def router_list_users():
return [{'id': 1, 'name': 'Alex'}]
@users_router.get('/users/{user_id}')
def router_get_user(user_id: int):
return {'id': user_id, 'name': 'Alex'}
print(type(users_router))
Part 3: include_router - Wiring It Into the App#
modular_app = FastAPI()
modular_app.include_router(users_router)
modular_client = TestClient(modular_app)
print(modular_client.get('/users').json())
print(modular_client.get('/users/7').json())
Part 4: prefix - Avoiding Repeated Path Segments#
products_router = APIRouter(prefix='/products')
@products_router.get('')
def router_list_products():
return [{'id': 1, 'name': 'Mug'}]
@products_router.get('/{product_id}')
def router_get_product(product_id: int):
return {'id': product_id, 'name': 'Mug'}
modular_app.include_router(products_router)
print(modular_client.get('/products').json())
print(modular_client.get('/products/3').json())
Part 5: tags on the Router - Applying to Every Route at Once#
orders_router = APIRouter(prefix='/orders', tags=['orders'])
@orders_router.get('')
def router_list_orders():
return [{'id': 100, 'total': 42.5}]
modular_app.include_router(orders_router)
schema = modular_app.openapi()
print(schema['paths']['/orders']['get']['tags'])
Part 6: Router-Level Dependencies#
from fastapi import Depends, Header, HTTPException
def require_admin(x_role: str = Header(default='guest')):
if x_role != 'admin':
raise HTTPException(status_code=403, detail='admin only')
admin_router = APIRouter(prefix='/admin', dependencies=[Depends(require_admin)])
@admin_router.get('/stats')
def admin_stats():
return {'users': 42}
modular_app.include_router(admin_router)
print(modular_client.get('/admin/stats', headers={'x-role': 'admin'}).status_code)
print(modular_client.get('/admin/stats').status_code)
Part 7: Multiple Independent Routers in One App#
final_app = FastAPI()
final_app.include_router(users_router)
final_app.include_router(products_router)
final_app.include_router(orders_router)
final_app.include_router(admin_router)
final_client = TestClient(final_app)
print(final_client.get('/users').status_code)
print(final_client.get('/products').status_code)
print(final_client.get('/orders').status_code)
Part 8: Nested Routers - Including One Router Inside Another#
reviews_router = APIRouter(prefix='/reviews')
@reviews_router.get('')
def list_reviews():
return [{'id': 1, 'stars': 5}]
products_v2_router = APIRouter(prefix='/products-v2')
products_v2_router.include_router(reviews_router)
nested_app = FastAPI()
nested_app.include_router(products_v2_router)
nested_client = TestClient(nested_app)
print(nested_client.get('/products-v2/reviews').json())
Part 9: prefix on include_router vs. prefix on the Router Itself#
generic_router = APIRouter()
@generic_router.get('/ping')
def ping():
return {'status': 'ok'}
mount_app = FastAPI()
mount_app.include_router(generic_router, prefix='/service-a')
mount_app.include_router(generic_router, prefix='/service-b')
mount_client = TestClient(mount_app)
print(mount_client.get('/service-a/ping').json())
print(mount_client.get('/service-b/ping').json())
Part 10: A Real Pattern - a Small Multi-Router Application#
health_router = APIRouter(tags=['health'])
@health_router.get('/health')
def health():
return {'status': 'ok'}
people_router = APIRouter(prefix='/people', tags=['people'])
@people_router.get('')
def list_people():
return [{'id': 1, 'name': 'Alex'}]
@people_router.get('/{person_id}')
def get_person(person_id: int):
return {'id': person_id, 'name': 'Alex'}
secure_router = APIRouter(prefix='/secure', tags=['secure'], dependencies=[Depends(require_admin)])
@secure_router.get('/report')
def secure_report():
return {'report': 'confidential'}
production_app = FastAPI(title='Demo Production API')
production_app.include_router(health_router)
production_app.include_router(people_router)
production_app.include_router(secure_router)
production_client = TestClient(production_app)
print(production_client.get('/health').json())
print(production_client.get('/people/1').json())
print(production_client.get('/secure/report', headers={'x-role': 'admin'}).json())
print(production_client.get('/secure/report').status_code)
Wrap-Up: What You Learned#
- APIRouter behaves like a small, separate FastAPI app for declaring endpoints.
- app.include_router copies every route from a router onto the app, fully wired in.
- prefix on a router gets prepended to every route it contains automatically.
- tags on a router applies the same docs grouping to every route it contains at once.
- dependencies on a router runs a shared check for every route it contains.
- Multiple independent routers can be included into the same app without interfering.
- A router can include_router another router too, nesting prefixes together.
- include_router's own prefix argument lets the same router be mounted more than once.
- A real production layout typically splits health, resource, and secured routes across routers.
- That wraps up APIRouter. Next up: Middleware and CORS.
Found this useful?
All lessons, notebooks and datasets here are free. If they helped you, a coffee keeps new lessons coming.



