Mathew K Analytics

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…

⬇ Download notebookOpen in Colab ↗

📓 Full notebook

Download .ipynb

FastAPI 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())
[{'id': 1, 'name': 'Alex'}]

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))
<class 'fastapi.routing.APIRouter'>

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())
[{'id': 1, 'name': 'Alex'}]
{'id': 7, 'name': 'Alex'}

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())
[{'id': 1, 'name': 'Mug'}]
{'id': 3, 'name': 'Mug'}

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'])
['orders']

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)
200
403

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)
200
200
200

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())
[{'id': 1, 'stars': 5}]

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())
{'status': 'ok'}
{'status': 'ok'}

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)
{'status': 'ok'}
{'id': 1, 'name': 'Alex'}
{'report': 'confidential'}
403

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.