FastAPI’s Depends() is one of the framework’s quiet superpowers. Used well, your handlers stay clean and testable. Used poorly, you have a tangle of dependencies that’s harder to understand than what they replace. This post is the working playbook.
The basic shape
from fastapi import Depends
def get_db():
db = SessionLocal()
try:
yield db
finally:
db.close()
@app.get("/users/{id}")
def get_user(id: int, db = Depends(get_db)):
return db.query(User).filter(User.id == id).first()
Depends(get_db) runs get_db, yields the value, runs the cleanup. Per request.
Async dependencies
async def get_db():
async with SessionLocal() as session:
yield session
@app.get("/users/{id}")
async def get_user(id: int, db: AsyncSession = Depends(get_db)):
return await db.get(User, id)
Async-native. Use async with for proper cleanup.
Auth dependency chain
async def get_token(authorization: str = Header(...)):
if not authorization.startswith("Bearer "):
raise HTTPException(401)
return authorization[7:]
async def get_current_user(token: str = Depends(get_token), db = Depends(get_db)):
user = await verify_and_load(token, db)
if not user:
raise HTTPException(401)
return user
async def get_admin(user = Depends(get_current_user)):
if not user.is_admin:
raise HTTPException(403)
return user
@app.get("/admin/users")
async def admin_list(user = Depends(get_admin)):
# only admins reach here
...
Chain dependencies. Each adds a layer.
Sub-dependencies
A dependency can have its own dependencies. Resolved automatically:
async def get_settings():
return load_settings()
async def get_db(settings = Depends(get_settings)):
return await connect(settings.database_url)
FastAPI builds a dependency graph; resolves once per request.
Caching within request
Same Depends(X) called twice in one request: X runs once.
async def get_user(...): ...
@app.get("/foo")
async def foo(user = Depends(get_user)): ...
# get_user only runs once even if also depended on transitively
use_cache=True is default. For “always re-resolve”: Depends(get_user, use_cache=False).
Class-based dependencies
class Pagination:
def __init__(self, page: int = 1, limit: int = Query(10, le=100)):
self.page = page
self.limit = limit
@app.get("/items")
def list_items(p: Pagination = Depends()):
return db.query(Item).offset((p.page - 1) * p.limit).limit(p.limit).all()
Useful for grouping related parameters. Especially nice when many endpoints share the same query params.
Lifespan dependencies
For app-level resources (DB pool, Redis connection):
from contextlib import asynccontextmanager
@asynccontextmanager
async def lifespan(app):
app.state.db_pool = await create_db_pool()
app.state.redis = await aioredis.from_url(...)
yield
await app.state.db_pool.close()
await app.state.redis.close()
app = FastAPI(lifespan=lifespan)
# Access via Request
async def get_redis(request: Request):
return request.app.state.redis
App startup → resources allocated; shutdown → cleanup.
Settings dependency
from functools import lru_cache
from pydantic_settings import BaseSettings
class Settings(BaseSettings):
database_url: str
secret_key: str
@lru_cache
def get_settings():
return Settings()
@app.get("/info")
def info(settings: Settings = Depends(get_settings)):
return {"db": settings.database_url[:20] + "..."}
lru_cache on the factory: settings loaded once, shared across requests. See Pydantic v2 Deep Dive
.
Background task injection
from fastapi import BackgroundTasks
@app.post("/signup")
async def signup(
user_data: UserIn,
bg: BackgroundTasks,
db: AsyncSession = Depends(get_db),
):
user = await create_user(db, user_data)
bg.add_task(send_welcome_email, user.email)
return user
BackgroundTasks is itself a special dependency. See FastAPI Background Tasks
.
Testing with dependency overrides
def test_get_user():
app.dependency_overrides[get_db] = lambda: TestDB()
app.dependency_overrides[get_current_user] = lambda: User(id=1, name="test")
response = client.get("/me")
assert response.json()["id"] == 1
app.dependency_overrides = {}
Or with a fixture:
@pytest.fixture
def client():
app.dependency_overrides[get_db] = lambda: TestDB()
yield TestClient(app)
app.dependency_overrides = {}
Cleanest way to test handlers without real DB / auth.
Multi-tenant dependency
async def get_tenant(request: Request) -> Tenant:
tenant_id = request.headers.get("x-tenant-id")
if not tenant_id:
raise HTTPException(400, "missing tenant")
return await load_tenant(tenant_id)
async def get_tenant_db(tenant: Tenant = Depends(get_tenant)) -> AsyncSession:
async with get_session_for_tenant(tenant) as session:
yield session
@app.get("/products")
async def products(db = Depends(get_tenant_db)):
return await db.query(Product).all()
Tenant-scoped DB session. See Multi-Tenancy Patterns .
When DI starts to bite
- Circular dependencies: A depends on B depends on A. Refactor.
- Deep graphs: 10-level chains hard to follow. Flatten.
- Mocking pain: many overrides per test. Maybe DI tax outweighs benefit.
- Implicit ordering: dependency order not obvious.
For these cases: consider explicit constructor injection in service classes; use FastAPI DI only at the request boundary.
Class-based service pattern
class UserService:
def __init__(self, db: AsyncSession, mailer: Mailer):
self.db = db
self.mailer = mailer
async def create(self, data):
user = await User.acreate(...)
await self.mailer.send_welcome(user.email)
return user
async def get_user_service(
db: AsyncSession = Depends(get_db),
mailer: Mailer = Depends(get_mailer),
) -> UserService:
return UserService(db, mailer)
@app.post("/users")
async def create_user(data: UserIn, svc: UserService = Depends(get_user_service)):
return await svc.create(data)
DI at the boundary; constructor injection inside services. Best of both.
Common mistakes
1. Heavy work in dependency factory
async def get_db():
return await create_pool() # creates pool PER REQUEST
Pool should be lifespan-scoped. Dependency just yields a session from the pool.
2. State in dependency function
counter = 0
async def get_counter():
global counter
counter += 1
return counter
Module-level state across requests = race conditions. Use request.state or app.state.
3. Synchronous dependency in async handler
def get_db(): # sync
return SessionLocal()
@app.get("/")
async def view(db = Depends(get_db)): # blocks event loop
...
Match async/sync.
4. Massive dependency chain
10-deep Depends(...). Refactor into services.
5. Forgetting cleanup
def get_db():
return SessionLocal() # leaks; never closed
Use yield + try/finally for cleanup.
What I’d ship today
For new FastAPI apps:
get_dbdependency for DB session (yield + cleanup).get_current_userchain for auth.- Settings via lru_cache.
- Lifespan for shared resources (pools).
- Service classes for business logic.
dependency_overridesin tests.
Read this next
- FastAPI + Pydantic v2 + SQLAlchemy 2.0
- FastAPI Streaming and SSE 2026
- FastAPI Background Tasks 2026
- Multi-Tenancy Patterns 2026
If you want my FastAPI starter (auth + DB + lifespan + service pattern), it’s at rajpoot.dev .
Building something AI-, backend-, or data-heavy and want a second pair of eyes? I do consulting and freelance work — see my projects and ways to reach me at rajpoot.dev .