APIRouter and Project Structure
Introduction
Large APIs split routes across modules with APIRouter—the FastAPI equivalent of Flask Blueprints. This chapter organizes /api/v1 endpoints, registers routers on the app, and uses lifespan hooks to open and close shared resources like database pools.
Prerequisites
- Response Models and Status Codes
- FastAPI Environment and Installation layout overview
Why Split Routes?
Single main.py breaks down when you have dozens of endpoints. Structure by domain:
app/
├── main.py
├── api/
│ └── v1/
│ ├── api.py # aggregates routers
│ └── endpoints/
│ ├── items.py
│ └── users.py
├── core/
│ └── config.py
└── schemas/
└── item.pyCompare Flask layout in Blueprints and Application Factory.
Define a Router
app/api/v1/endpoints/items.py:
from fastapi import APIRouter
router = APIRouter()
@router.get("/")
def list_items():
return [{"id": 1, "name": "Demo"}]
@router.get("/{item_id}")
def get_item(item_id: int):
return {"id": item_id, "name": "Demo"}app/api/v1/endpoints/users.py:
from fastapi import APIRouter
router = APIRouter()
@router.get("/me")
def read_me():
return {"username": "demo"}Aggregate and Register
app/api/v1/api.py:
from fastapi import APIRouter
from app.api.v1.endpoints import items, users
api_router = APIRouter()
api_router.include_router(items.router, prefix="/items", tags=["items"])
api_router.include_router(users.router, prefix="/users", tags=["users"])app/main.py:
from fastapi import FastAPI
from app.api.v1.api import api_router
app = FastAPI(title="Learning API")
app.include_router(api_router, prefix="/api/v1")
@app.get("/health")
def health():
return {"status": "ok"}URLs:
| Route function | URL |
|---|---|
list_items | GET /api/v1/items/ |
get_item | GET /api/v1/items/{item_id} |
read_me | GET /api/v1/users/me |
health | GET /health |
Code explanation:
prefixstacks:/api/v1+/items+ route pathtagsgroup endpoints in Swagger UI
Lifespan Context (Modern Startup/Shutdown)
Replace deprecated @app.on_event("startup") with:
from contextlib import asynccontextmanager
from fastapi import FastAPI
@asynccontextmanager
async def lifespan(app: FastAPI):
# startup: connect DB pool, warm caches
print("Starting up")
yield
# shutdown: close connections
print("Shutting down")
app = FastAPI(lifespan=lifespan)Code explanation:
- Code before
yieldruns at startup - Code after
yieldruns at shutdown - Works with async resource cleanup
Sync startup alternative for small apps:
@asynccontextmanager
async def lifespan(app: FastAPI):
init_db()
yield
close_db()Versioning
| Strategy | Example |
|---|---|
| URL prefix | /api/v1, /api/v2 |
| Separate router packages | app/api/v1/, app/api/v2/ |
Keep v1 stable; add v2 router without deleting v1 until clients migrate.
Tags and OpenAPI Organization
router = APIRouter(prefix="/admin", tags=["admin"])Tags appear as sections in /docs—match your team’s bounded contexts.
Run the Structured App
uvicorn app.main:app --reload
curl http://127.0.0.1:8000/api/v1/items/
curl http://127.0.0.1:8000/healthFAQ
Router not found 404?
Check include_router prefix chain and trailing slashes—/items vs /items/ can differ.
Circular imports?
main.py imports routers; routers should not import app from main. Use deps.py for shared dependencies.
Same route name collision?
Operation IDs in OpenAPI must be unique—set operation_id on routes if needed.
Mount static files?
app.mount("/static", StaticFiles(...)) on FastAPI instance—static files chapter.
Sub-application?
app.mount("/sub", sub_app) for rare composite setups.
Test router in isolation?
Import router into test app: FastAPI(); app.include_router(router).