Skip to main content

Reusable Dependencies

A dependency is just a Python function. When you design it to do one thing well and compose it with other dependencies, it becomes a powerful building block that keeps your routes clean and your logic testable.

Learning Focus

By the end of this lesson you can: build reusable authentication, rate limiting, and database dependencies — and override them cleanly in tests with app.dependency_overrides.

Database Session Dependency

The canonical pattern for async SQLAlchemy sessions:

app/db/session.py
from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine, async_sessionmaker
from app.core.config import settings

engine = create_async_engine(settings.DATABASE_URL, echo=False, pool_size=10)
AsyncSessionFactory = async_sessionmaker(engine, expire_on_commit=False)

async def get_db() -> AsyncSession:
async with AsyncSessionFactory() as session:
try:
yield session
await session.commit()
except Exception:
await session.rollback()
raise
app/routers/users.py
from typing import Annotated
from fastapi import APIRouter, Depends
from sqlalchemy.ext.asyncio import AsyncSession
from app.db.session import get_db

DBSession = Annotated[AsyncSession, Depends(get_db)]

@router.get("/{user_id}")
async def get_user(user_id: int, db: DBSession) -> dict:
result = await db.get(UserORM, user_id)
...

Authentication Dependency

app/dependencies/auth.py
from typing import Annotated
from fastapi import Depends, HTTPException, status
from fastapi.security import OAuth2PasswordBearer
from jose import JWTError, jwt
from app.core.config import settings

oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/auth/token")

async def get_current_user(
token: Annotated[str, Depends(oauth2_scheme)],
db: Annotated[AsyncSession, Depends(get_db)],
) -> UserORM:
credentials_exception = HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Could not validate credentials",
headers={"WWW-Authenticate": "Bearer"},
)
try:
payload = jwt.decode(token, settings.SECRET_KEY, algorithms=["HS256"])
user_id: int = payload.get("sub")
if user_id is None:
raise credentials_exception
except JWTError:
raise credentials_exception

user = await db.get(UserORM, user_id)
if user is None:
raise credentials_exception
return user

CurrentUser = Annotated[UserORM, Depends(get_current_user)]

Role-Based Access Dependency

app/dependencies/auth.py
def require_role(role: str):
async def _check(user: CurrentUser) -> UserORM:
if user.role != role:
raise HTTPException(403, f"Role '{role}' required")
return user
return _check

AdminUser = Annotated[UserORM, Depends(require_role("admin"))]
app/routers/admin.py
from app.dependencies.auth import AdminUser

@router.get("/admin/users")
async def list_all_users(admin: AdminUser, db: DBSession) -> list:
...

Feature Flag Dependency

app/dependencies/features.py
from fastapi import HTTPException
from app.core.config import settings

def feature_enabled(flag_name: str):
async def _check() -> None:
if not getattr(settings, flag_name, False):
raise HTTPException(404, "Feature not available")
return _check

BetaFeature = Depends(feature_enabled("BETA_ENABLED"))

Overriding Dependencies in Tests

This is why DI is so valuable — you can swap any dependency in tests without mocking:

tests/conftest.py
import pytest
from fastapi.testclient import TestClient
from app.main import app
from app.db.session import get_db
from app.dependencies.auth import get_current_user

@pytest.fixture
def client():
# Override DB with a test session
async def override_get_db():
yield test_session # Use a test database session

# Override auth — always return a fake admin user
async def override_get_current_user():
return {"id": 1, "username": "testuser", "role": "admin"}

app.dependency_overrides[get_db] = override_get_db
app.dependency_overrides[get_current_user] = override_get_current_user

yield TestClient(app)

app.dependency_overrides.clear()

Common Pitfalls

PitfallCause / SymptomFix
DB session not committedForgot yield session; await session.commit()Use the try/yield/except pattern
Auth dep not protecting routeForgot to add Depends(get_current_user)Add it to the function signature or router-level dependencies
Overrides not cleared after testOther tests affected by leaked overridesAlways call app.dependency_overrides.clear() in test teardown
Nested dep returns wrong typeDep chain returns a string instead of a modelAnnotate every dependency's return type explicitly
Depends used in background taskBackground tasks don't participate in request scopePass resources explicitly, not via Depends in background tasks

Hands-On Practice

app/dependencies/request_id.py
import uuid
from typing import Annotated
from fastapi import Depends, Request, Response

async def request_id(request: Request, response: Response) -> str:
"""Attach a unique request ID to every request and response."""
rid = request.headers.get("X-Request-ID", str(uuid.uuid4()))
response.headers["X-Request-ID"] = rid
return rid

RequestID = Annotated[str, Depends(request_id)]
app/routers/api.py
from fastapi import APIRouter
from app.dependencies.request_id import RequestID

router = APIRouter()

@router.get("/ping")
async def ping(request_id: RequestID) -> dict:
return {"pong": True, "request_id": request_id}
test-request-id.sh
curl -v http://localhost:8000/ping
# Response header: X-Request-ID: <uuid>

curl -v http://localhost:8000/ping -H "X-Request-ID: my-custom-id"
# Response header: X-Request-ID: my-custom-id

What's Next