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_overrides to swap real services for test doubles.
  • Use closure-based dependency factories to create configurable, reusable permission checks.

Leave a Comment

Your email address will not be published. Required fields are marked *