Path Operations and Decorators
A path operation in FastAPI terminology is a combination of an HTTP method and a URL path. The decorator @app.get("/items/") defines a GET path operation. Understanding how these decorators work, and how to organize them with APIRouter, is fundamental to building maintainable APIs.
By the end of this lesson you can: use all HTTP method decorators, add metadata to operations, organize routes with APIRouter, and include routers in the main app with prefixes and tags.
HTTP Method Decorators
FastAPI provides a decorator for each standard HTTP method:
from fastapi import FastAPI
app = FastAPI()
@app.get("/items/") # Read / list
async def list_items(): ...
@app.post("/items/") # Create
async def create_item(): ...
@app.put("/items/{id}") # Full update (replace)
async def update_item(id: int): ...
@app.patch("/items/{id}") # Partial update
async def patch_item(id: int): ...
@app.delete("/items/{id}") # Delete
async def delete_item(id: int): ...
@app.head("/items/") # Like GET but no body
async def head_items(): ...
@app.options("/items/") # CORS preflight, capability check
async def options_items(): ...
| Method | Idempotent | Body | Common use |
|---|---|---|---|
| GET | ✅ | No | Read resource |
| POST | ❌ | Yes | Create resource |
| PUT | ✅ | Yes | Replace resource |
| PATCH | ❌ | Yes | Partial update |
| DELETE | ✅ | Optional | Remove resource |
Operation Metadata
Add metadata to each operation for better OpenAPI documentation:
from fastapi import APIRouter
router = APIRouter()
@router.get(
"/items/",
tags=["items"],
summary="List all items",
description="Returns a paginated list of all items in the system.",
response_description="A list of item objects",
operation_id="list_items", # Unique ID for client SDK generation
deprecated=False,
)
async def list_items(skip: int = 0, limit: int = 10) -> list[dict]:
return []
operation_id must be unique across all routes. FastAPI auto-generates one from the function name if you don't set it. Set it explicitly when generating client SDKs.
APIRouter — Organizing Routes into Modules
Putting all routes in main.py does not scale. Use APIRouter to group related routes into separate files:
from fastapi import APIRouter
router = APIRouter(
prefix="/items", # All routes here are under /items
tags=["items"], # Tag applied to all routes in this router
responses={404: {"description": "Item not found"}},
)
@router.get("/")
async def list_items() -> list[dict]:
return []
@router.get("/{item_id}")
async def get_item(item_id: int) -> dict:
return {"id": item_id}
@router.post("/", status_code=201)
async def create_item() -> dict:
return {"created": True}
from fastapi import FastAPI
from app.routers import items, users
app = FastAPI()
app.include_router(items.router)
app.include_router(users.router, prefix="/api/v1") # Override prefix
Route Order Matters
FastAPI matches routes in the order they are declared. Put more specific patterns before generic ones:
from fastapi import APIRouter
router = APIRouter(prefix="/users", tags=["users"])
# ✅ Specific routes FIRST
@router.get("/me")
async def get_current_user() -> dict:
return {"user": "me"}
# ✅ Generic path params AFTER
@router.get("/{user_id}")
async def get_user(user_id: int) -> dict:
return {"user_id": user_id}
If /{user_id} came before /me, FastAPI would try to coerce "me" to int, fail, and return a 422 error.
Including Routers with Overrides
You can override tags, prefix, and responses when including a router:
from fastapi import FastAPI
from app.routers import items
app = FastAPI()
# Override defaults set in the router itself
app.include_router(
items.router,
prefix="/api/v2",
tags=["items-v2"],
dependencies=[], # Add global dependencies for this router
)
Nested Routers
Routers can include other routers:
from fastapi import APIRouter
from app.routers import items, users
api_router = APIRouter(prefix="/api/v1")
api_router.include_router(items.router)
api_router.include_router(users.router)
from app.routers.api import api_router
app.include_router(api_router)
# Final routes: /api/v1/items/, /api/v1/users/
Common Pitfalls
| Pitfall | Cause / Symptom | Fix |
|---|---|---|
| 404 for a valid path | Router not included with include_router | Always call app.include_router(router) in main.py |
Double prefix (/api/v1/api/v1/items) | Prefix set in both router and include_router call | Set prefix in only one place |
Same operation_id on two routes | OpenAPI spec validation error, broken SDK generation | Use unique operation_id or let FastAPI auto-generate |
/me returns 422 instead of the user | /me declared after /{user_id} | Move specific routes before parametric routes |
| Tags not appearing in docs | Tags not set at decorator or router level | Set tags=[...] on router or each decorator |
Hands-On Practice
from fastapi import APIRouter, HTTPException
from pydantic import BaseModel
router = APIRouter(prefix="/products", tags=["products"])
class Product(BaseModel):
id: int
name: str
price: float
_db: dict[int, Product] = {
1: Product(id=1, name="Widget", price=9.99),
2: Product(id=2, name="Gadget", price=29.99),
}
@router.get("/", summary="List all products")
async def list_products() -> list[Product]:
return list(_db.values())
@router.get("/featured", summary="Get featured product")
async def get_featured() -> Product:
return _db[1]
@router.get("/{product_id}", summary="Get one product")
async def get_product(product_id: int) -> Product:
if product_id not in _db:
raise HTTPException(404, detail="Product not found")
return _db[product_id]
from fastapi import FastAPI
from app.routers.products import router as products_router
app = FastAPI(title="Router Practice")
app.include_router(products_router)
uvicorn app.main:app --reload
# List all
curl http://localhost:8000/products/
# Featured
curl http://localhost:8000/products/featured
# Single
curl http://localhost:8000/products/1
# Not found
curl http://localhost:8000/products/99 -w "\nHTTP %{http_code}"