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

Why Split Routes?

Single main.py breaks down when you have dozens of endpoints. Structure by domain:

text
app/
├── main.py
├── api/
│   └── v1/
│       ├── api.py           # aggregates routers
│       └── endpoints/
│           ├── items.py
│           └── users.py
├── core/
│   └── config.py
└── schemas/
    └── item.py

Compare Flask layout in Blueprints and Application Factory.

Define a Router

app/api/v1/endpoints/items.py:

python
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:

python
from fastapi import APIRouter
 
router = APIRouter()
 
@router.get("/me")
def read_me():
    return {"username": "demo"}

Aggregate and Register

app/api/v1/api.py:

python
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:

python
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 functionURL
list_itemsGET /api/v1/items/
get_itemGET /api/v1/items/{item_id}
read_meGET /api/v1/users/me
healthGET /health

Code explanation:

  • prefix stacks: /api/v1 + /items + route path
  • tags group endpoints in Swagger UI

Lifespan Context (Modern Startup/Shutdown)

Replace deprecated @app.on_event("startup") with:

python
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 yield runs at startup
  • Code after yield runs at shutdown
  • Works with async resource cleanup

Sync startup alternative for small apps:

python
@asynccontextmanager
async def lifespan(app: FastAPI):
    init_db()
    yield
    close_db()

Versioning

StrategyExample
URL prefix/api/v1, /api/v2
Separate router packagesapp/api/v1/, app/api/v2/

Keep v1 stable; add v2 router without deleting v1 until clients migrate.

Tags and OpenAPI Organization

python
router = APIRouter(prefix="/admin", tags=["admin"])

Tags appear as sections in /docs—match your team’s bounded contexts.

Run the Structured App

bash
uvicorn app.main:app --reload
curl http://127.0.0.1:8000/api/v1/items/
curl http://127.0.0.1:8000/health

FAQ

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