Dependency Injection — Cheat Sheet
FastAPI · 3 topics. Download the PDF or the Instagram carousel and share it.
Cheat Sheet · AiCanCode.org
Dependency Injection
FastAPI3 topicsQuick revision reference
1
Depends — Dependency Injection Basics
Depends(fn) tells FastAPI: run fn first (resolving ITS parameters the same way) and hand me the result. Shared logic — pagination, auth, DB sessions — becomes declared-once, injected-everywhere.
- ✓Depends(fn): fn runs first, its params bind from the request, you get the result
- ✓Annotated[T, Depends(fn)] aliases make dependencies reusable one-liners
- ✓Dependencies nest into trees; FastAPI resolves the whole graph
- ✓Per-request caching: the same dependency runs once per request by default
Declare pagination once; every list endpoint inherits it
from typing import Annotated
from fastapi import Depends, FastAPI, Query
app = FastAPI()
# A dependency is just a function — its params bind like any endpoint's
def pagination(
page: int = Query(default=1, ge=1),
size: int = Query(default=20, ge=1, le=100),
) -> dict:
return {"offset": (page - 1) * size, "limit": size}
# Annotated alias — define once, reuse as a type (modern style)
Pagination = Annotated[dict, Depends(pagination)]
@app.get("/students")
def list_students(p: Pagination):
return {"slice": f"students[{p['offset']}:{p['offset'] + p['limit']}]"}
@app.get("/drives")
def list_drives(p: Pagination): # same rules, zero duplication
return {"slice": f"drives[{p['offset']}:{p['offset'] + p['limit']}]"}
# GET /students?page=3&size=50 → {"slice": "students[100:150]"}
# GET /students?size=500 → 422 (le=100) — enforced EVERYWHERE at once
# /docs shows page & size on both endpoints — dependencies are documented too2
Dependencies with yield & Classes — Setup and Teardown
A yield dependency runs setup, hands over the resource, then runs cleanup after the response — the DB-session pattern. Class dependencies carry configuration; both compose with everything else.
- ✓yield dependencies: setup before, teardown after the response, finally-guaranteed
- ✓The get_db commit/rollback/close pattern is the canonical use
- ✓Class with __call__ = configurable dependency (limits, flags, permissions)
- ✓dependencies=[Depends(x)] runs a dependency for its effect without injecting a value
Before yield = setup; after = teardown; finally = guaranteed
from fastapi import Depends, FastAPI
app = FastAPI()
class FakeSession: # stand-in for SQLAlchemy Session
def __init__(self): print("OPEN session")
def commit(self): print("COMMIT")
def rollback(self): print("ROLLBACK")
def close(self): print("CLOSE session")
def add(self, x): pass
def get_db():
db = FakeSession() # 1. setup — runs before endpoint
try:
yield db # 2. injected value; endpoint runs here
db.commit() # 3a. endpoint succeeded → commit
except Exception:
db.rollback() # 3b. endpoint raised → rollback
raise # re-raise so handlers still run
finally:
db.close() # 4. ALWAYS — even on errors
@app.post("/students")
def create_student(payload: dict, db: FakeSession = Depends(get_db)):
db.add(payload)
return {"ok": True}
# Success log: OPEN session → (endpoint) → COMMIT → CLOSE session
# Failure log: OPEN session → (endpoint raises) → ROLLBACK → CLOSE session
#
# The same shape manages anything with a lifecycle:
# temp files, redis pipelines, distributed locks, tracing spans.3
DI Patterns — Auth Chains, Router Guards & Test Overrides
Real apps stack dependencies into auth chains (token → user → role check), guard whole routers with one line, and swap any node via dependency_overrides in tests — the architecture is the dependency graph.
- ✓Auth = dependency chain: token → user → role; endpoints declare their guard
- ✓require_role(*roles) is a dependency factory — closures carrying config
- ✓401 = not authenticated, 403 = authenticated but not permitted
- ✓Router/app-level dependencies guard everything; dependency_overrides swaps nodes in tests
token → user → role: each endpoint declares its guard level
from typing import Annotated
from fastapi import Depends, FastAPI, HTTPException, Header
app = FastAPI()
FAKE_USERS = {"token-asha": {"name": "Asha", "role": "student"},
"token-tpo": {"name": "Rane", "role": "tpo"}}
def get_token(authorization: str | None = Header(default=None)) -> str:
if not authorization or not authorization.startswith("Bearer "):
raise HTTPException(401, "not authenticated") # 401: who are you?
return authorization.removeprefix("Bearer ")
def get_current_user(token: Annotated[str, Depends(get_token)]) -> dict:
user = FAKE_USERS.get(token) # JWT decode + DB load in real life
if not user:
raise HTTPException(401, "invalid or expired token")
return user
CurrentUser = Annotated[dict, Depends(get_current_user)]
def require_role(*roles: str): # ← dependency FACTORY
def checker(user: CurrentUser) -> dict:
if user["role"] not in roles:
raise HTTPException(403, "insufficient permissions") # 403: not you
return user
return checker
@app.get("/profile")
def profile(user: CurrentUser): # any logged-in user
return user
@app.post("/drives")
def create_drive(user: Annotated[dict, Depends(require_role("tpo"))]):
return {"created_by": user["name"]}
# token-asha on POST /drives → 403 · no token → 401
# The signature IS the access-control documentation.Learn this free with Aria, your AI tutor → AiCanCode.org/learn/fastapi