FastAPI: From Basics to Production · Building Real APIs
Routers for Separation
Split an app into feature routers with prefixes, tags and shared dependencies.
APIRouter groups related endpoints into modules (users, orders, admin), each with its own prefix, tags, dependencies and responses, then app.include_router() mounts them. A common layout: one package per feature containing router.py, schemas.py, models.py and service.py, keeping HTTP concerns in routers and business logic in services.
# app/orders/router.py
from fastapi import APIRouter, Depends
router = APIRouter(prefix="/orders", tags=["orders"], dependencies=[Depends(require_user)])
@router.get("")
async def list_orders(db: DB, user: CurrentUser) -> list[OrderOut]:
return await service.list_for_user(db, user.id)
@router.post("", status_code=201)
async def create_order(body: OrderCreate, db: DB, user: CurrentUser) -> OrderOut:
return await service.create(db, user.id, body)
# app/main.py
from app.orders.router import router as orders_router
from app.admin.router import router as admin_router
app = FastAPI(lifespan=lifespan)
app.include_router(orders_router, prefix="/api/v1")
app.include_router(admin_router, prefix="/api/v1", dependencies=[Depends(require_admin)])
Common pitfalls
- One giant
main.pywith every endpoint. - Business logic inside route functions, making it hard to reuse from jobs or tests.
- Circular imports between feature modules.
Key points
- One router per feature
- Prefixes, tags and dependencies per router
- Keep logic in services
Further reading: FastAPI: Bigger applications
Practice questions
Why keep business logic out of route functions?
Answer
Services can then be reused from CLI scripts, background jobs and tests without HTTP, and routes stay thin adapters for validation and responses.
How do you protect every admin route with one line?
Answer
Include the admin router with dependencies=[Depends(require_admin)], so the check runs for all its routes.
Open in the app to mark lessons complete, take notes and track your progress.