Overview
FastAPI’s Depends() mechanism is the primary way to share resources across routes without threading state through function arguments. It handles database sessions, authentication, settings, and feature flags. The dependency graph is resolved per-request, cached within a request, and composable to arbitrary depth. This page expands on the Depends section in fastapi with patterns for nested deps, class-based deps, and cleanup. See fastapi-lifespan for application-scoped resources that live across requests.
Keep route functions thin by pushing logic into dependencies
A route function should declare what it needs, then delegate. Business logic, access control, and resource acquisition belong in dependencies, not in the route body.
from fastapi import APIRouter, Depends
from sqlalchemy.ext.asyncio import AsyncSession
router = APIRouter()
@router.get("/orders/{order_id}", response_model=OrderRead)
async def get_order(
order_id: str,
order: Order = Depends(get_order_or_404),
user: User = Depends(get_current_user),
) -> Order:
require_permission(user, "orders:read", order)
return orderThe route itself does one thing: check permission and return. Fetching the order and resolving the user are separate dependency functions tested independently.
Chain dependencies to express context hierarchy
A dependency can itself declare Depends(). FastAPI builds a directed acyclic graph and resolves each node once per request.
async def get_db() -> AsyncGenerator[AsyncSession, None]:
async with async_session_factory() as session:
yield session
async def get_current_user(
token: str = Depends(oauth2_scheme),
db: AsyncSession = Depends(get_db),
) -> User:
return await auth_service.resolve(db, token)
async def get_admin_user(user: User = Depends(get_current_user)) -> User:
if not user.is_admin:
raise HTTPException(status_code=403)
return userget_db runs once per request even if both get_current_user and the route body declare it. The session object is cached at the request scope. See fastapi-async-io for why the session must be async.
Use yield for resource cleanup
A dependency that opens a resource should yield it, then close it. FastAPI calls the code after yield after the response is sent.
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker
SessionLocal: async_sessionmaker[AsyncSession] = ... # set in lifespan
async def get_db() -> AsyncGenerator[AsyncSession, None]:
async with SessionLocal() as session:
try:
yield session
await session.commit()
except Exception:
await session.rollback()
raiseWrap the yield in a try/except to roll back on errors. FastAPI catches exceptions from dependencies and re-raises them after running cleanup. Never hold a database connection across yield longer than one request; see postgres for connection pool sizing.
Prefer class-based dependencies for configurable behavior
When a dependency needs configuration, a class with __call__ is cleaner than a factory of closures.
class RateLimiter:
def __init__(self, max_calls: int, window_seconds: int) -> None:
self.max_calls = max_calls
self.window_seconds = window_seconds
async def __call__(self, request: Request) -> None:
key = f"rl:{request.client.host}"
if await exceeds_limit(key, self.max_calls, self.window_seconds):
raise HTTPException(status_code=429)
strict_limit = RateLimiter(max_calls=10, window_seconds=60)
@router.post("/payments", dependencies=[Depends(strict_limit)])
async def create_payment(...) -> PaymentRead: ...Class instances are reused across requests. Stateful dependencies (counters, caches) stored on the instance will be shared; use external storage or app.state for anything that must persist. See fastapi-lifespan for initializing shared state.
Use Annotated to DRY repeated dependency declarations
Annotated from typing lets you name a type-plus-dependency pair once and reuse it.
from typing import Annotated
from fastapi import Depends
CurrentUser = Annotated[User, Depends(get_current_user)]
DBSession = Annotated[AsyncSession, Depends(get_db)]
@router.get("/profile")
async def profile(user: CurrentUser, db: DBSession) -> UserRead:
return await user_service.get(db, user.id)This removes repeated = Depends(...) boilerplate across dozens of routes and makes the dependency visible in the type signature. See fastapi-pydantic for how these typed values integrate with response models.
Override dependencies in tests without monkey-patching
FastAPI provides app.dependency_overrides to swap a dependency in tests without patching global state.
from fastapi.testclient import TestClient
def fake_db():
yield test_session
app.dependency_overrides[get_db] = fake_db
client = TestClient(app)Reset app.dependency_overrides = {} in teardown to avoid state leaking across tests. See python-testing for the full fixture and isolation strategy.