FastAPI Advanced Dependency Injection Patterns
Basic dependency injection covered in Topic 13 handles simple shared logic. This topic goes deeper — scoped dependencies, yield-based teardown, overriding dependencies in tests, and building a clean service layer that keeps your route functions thin.
Yield Dependencies — Setup and Teardown
A dependency that uses yield runs setup code before the route and teardown code after the response. The database session pattern from Topic 17 uses this:
def get_db():
db = SessionLocal() ← setup: open session
try:
yield db ← route runs here with db
finally:
db.close() ← teardown: always closes
Timeline:
Request arrives
↓
get_db() runs setup: db = SessionLocal()
↓
yield db → route function runs
↓
Response sent to client
↓
get_db() runs teardown: db.close()
This pattern works for any resource that must be opened and closed: file handles, network connections, locks.
Service Layer Pattern
Instead of writing database queries inside route functions, put them in a service class and inject the service:
# services/user_service.py
from sqlalchemy.orm import Session
import models
class UserService:
def __init__(self, db: Session):
self.db = db
def get_by_id(self, user_id: int):
return self.db.query(models.User).filter(models.User.id == user_id).first()
def create(self, name: str, email: str):
user = models.User(name=name, email=email)
self.db.add(user)
self.db.commit()
self.db.refresh(user)
return user
# dependencies.py
from fastapi import Depends
from sqlalchemy.orm import Session
from database import get_db
from services.user_service import UserService
def get_user_service(db: Session = Depends(get_db)) -> UserService:
return UserService(db)
# routes
@app.get("/users/{user_id}")
def get_user(user_id: int, service: UserService = Depends(get_user_service)):
user = service.get_by_id(user_id)
if not user:
raise HTTPException(404, "Not found")
return user
Route function does: find user, handle 404, return Service does: all database interaction Result: thin, readable routes; reusable service
Caching Dependencies with use_cache
By default, FastAPI calls each dependency only once per request even if multiple routes in the chain depend on it. This is the cache:
def get_db():
print("Opening DB")
db = SessionLocal()
try:
yield db
finally:
db.close()
def get_user(db = Depends(get_db)): ← uses cached db
...
def get_settings(db = Depends(get_db)): ← same db, not called again
...
To disable caching and get a fresh instance every time, pass use_cache=False:
Depends(get_db, use_cache=False)
Overriding Dependencies in Tests
This is one of the most powerful features. Replace a real dependency with a fake one during testing without changing any route code:
# tests/test_users.py
from fastapi.testclient import TestClient
from main import app
from database import get_db
def override_get_db():
db = TestSessionLocal() ← test database
try:
yield db
finally:
db.close()
app.dependency_overrides[get_db] = override_get_db
client = TestClient(app)
def test_get_users():
response = client.get("/users")
assert response.status_code == 200
Production: get_db() → real PostgreSQL database Tests: get_db() → in-memory SQLite database Same route code. Different dependency. No changes needed.
Parameterized Dependencies with Closures
Create a dependency factory that accepts configuration and returns a dependency function:
def require_role(role: str):
def check_role(current_user = Depends(get_current_user)):
if current_user.role != role:
raise HTTPException(403, f"Requires role: {role}")
return current_user
return check_role
@app.get("/admin/stats")
def admin_stats(user = Depends(require_role("admin"))):
return {"data": "admin only"}
@app.get("/manager/reports")
def manager_reports(user = Depends(require_role("manager"))):
return {"data": "manager only"}
Key Points
- Yield dependencies run setup before the route and teardown after the response — perfect for resources that need cleanup.
- The service layer pattern moves database logic into classes, keeping route functions small.
- FastAPI caches dependency results within a single request by default — each dependency runs once.
- Override dependencies in tests using
app.dependency_overridesto swap real services for test doubles. - Use closure-based dependency factories to create configurable, reusable permission checks.
