RESTful Route Design with FastAPI Router
Organize routes with APIRouter, path parameters, query parameters, and dependency injection
The Problem
All routes in one file becomes unmanageable at 10+ endpoints. You also need consistent URL patterns, proper HTTP methods, and a way to share dependencies (auth, DB sessions) across routes.
APIRouter — Modular Route Organization
# app/api/routes/items.py
import uuid
from fastapi import APIRouter, HTTPException
from app.api.models.items import ItemCreate, ItemListResponse, ItemResponse
from app.logging_config import get_logger
router = APIRouter(prefix="/api/v1/items", tags=["items"])
logger = get_logger(__name__)
# In-memory store (replace with a real DB)
_items: dict[str, ItemResponse] = {}
prefix="/api/v1/items" — URL Namespace
All routes in this router start with /api/v1/items. The route decorator only specifies the sub-path:
@router.post("/") # → POST /api/v1/items/
@router.get("/") # → GET /api/v1/items/
@router.get("/{item_id}") # → GET /api/v1/items/{item_id}
tags=["items"] — OpenAPI Grouping
Routes are grouped under “items” in Swagger UI. Makes large APIs navigable.
CRUD Implementation
Create (POST)
@router.post("/", response_model=ItemResponse, status_code=201)
async def create_item(item: ItemCreate) -> ItemResponse:
item_id = str(uuid.uuid4())
created = ItemResponse(id=item_id, **item.model_dump())
_items[item_id] = created
await logger.ainfo("item_created", item_id=item_id, name=item.name)
return created
status_code=201— HTTP 201 Created, not the default 200response_model=ItemResponse— Response schema for OpenAPI docs
List (GET)
@router.get("/", response_model=ItemListResponse)
async def list_items() -> ItemListResponse:
items = list(_items.values())
await logger.ainfo("items_listed", count=len(items))
return ItemListResponse(items=items, total=len(items))
Returns a wrapper with items array and total count — pagination-ready.
Get by ID (GET)
@router.get("/{item_id}", response_model=ItemResponse)
async def get_item(item_id: str) -> ItemResponse:
item = _items.get(item_id)
if not item:
await logger.awarning("item_not_found", item_id=item_id)
raise HTTPException(status_code=404, detail="Item not found")
return item
- Path parameter
{item_id}is extracted and typed automatically HTTPException(status_code=404)returns proper HTTP error responses
Delete (DELETE)
@router.delete("/{item_id}", status_code=204)
async def delete_item(item_id: str) -> None:
if item_id not in _items:
await logger.awarning("item_not_found", item_id=item_id)
raise HTTPException(status_code=404, detail="Item not found")
del _items[item_id]
await logger.ainfo("item_deleted", item_id=item_id)
status_code=204— No Content, standard for successful deletes- Return type
None— no response body
Health Routes — A Separate Router
# app/api/routes/health.py
from fastapi import APIRouter
from app.config import settings
router = APIRouter(tags=["health"])
@router.get("/health")
async def health_check() -> dict:
return {
"status": "healthy",
"service": settings.app_name,
"version": settings.app_version,
"environment": settings.app_env,
}
@router.get("/ready")
async def readiness_check() -> dict:
return {"status": "ready"}
No prefix — health and readiness at root level: /health and /ready.
Registering Routers
# app/main.py
app.include_router(health_router)
app.include_router(items_router)
Each router is a separate module. Add new domains by creating a new file and calling include_router().
REST Conventions Used
| Operation | Method | Path | Status |
|---|---|---|---|
| Create | POST | /api/v1/items/ | 201 |
| List | GET | /api/v1/items/ | 200 |
| Get one | GET | /api/v1/items/{id} | 200 |
| Delete | DELETE | /api/v1/items/{id} | 204 |
| Not found | — | — | 404 |
| Validation error | — | — | 422 |
Next Step
In the next lesson, we build consistent error handling — exception handlers, structured error responses, and HTTP status code conventions.