Skip to main content

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.

Learning Focus

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:

app/main.py
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(): ...
MethodIdempotentBodyCommon use
GETNoRead resource
POSTYesCreate resource
PUTYesReplace resource
PATCHYesPartial update
DELETEOptionalRemove resource

Operation Metadata

Add metadata to each operation for better OpenAPI documentation:

app/routers/items.py
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 []
note

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:

app/routers/items.py
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}
app/main.py
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:

app/routers/users.py
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}
warning

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:

app/main.py
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:

app/routers/api.py
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)
app/main.py
from app.routers.api import api_router

app.include_router(api_router)
# Final routes: /api/v1/items/, /api/v1/users/

Common Pitfalls

PitfallCause / SymptomFix
404 for a valid pathRouter not included with include_routerAlways call app.include_router(router) in main.py
Double prefix (/api/v1/api/v1/items)Prefix set in both router and include_router callSet prefix in only one place
Same operation_id on two routesOpenAPI spec validation error, broken SDK generationUse 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 docsTags not set at decorator or router levelSet tags=[...] on router or each decorator

Hands-On Practice

app/routers/products.py
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]
app/main.py
from fastapi import FastAPI
from app.routers.products import router as products_router

app = FastAPI(title="Router Practice")
app.include_router(products_router)
test-router.sh
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}"

What's Next