FastAPI Dependency Injection Explained
How FastAPI's Depends system actually works, the lifecycle of dependencies, scoping with yield, and patterns for testable, layered FastAPI apps.
What you'll learn
- ✓How Depends resolves and caches per request
- ✓Yield dependencies and resource lifecycle
- ✓Layering: settings -> session -> repository -> service
- ✓Dependency overrides for tests
- ✓Common DI pitfalls in FastAPI
Prerequisites
- •Basic Python
- •Some exposure to FastAPI
What and why
FastAPI’s dependency injection is the most underused part of the framework. Most tutorials show it as a way to read query parameters, then move on. In real apps, Depends is how you compose your application: configuration, database sessions, authentication, repositories, and feature flags all flow through it.
The win is testability. Anything injected via Depends can be swapped at test time without monkeypatching. The cost is learning how the resolver behaves around scopes and yields.
Mental model
For every incoming request, FastAPI walks the route handler’s signature, finds every Depends(...), and builds a dependency graph. Each unique dependency is resolved once per request and cached. Generators (yield) double as setup/teardown: code before yield runs at injection, code after runs after the response is sent.
Request --> route handler(user, db, settings)
|
v
build dependency graph
|
+-------------+-------------+
| | |
get_settings get_db get_user
(cached) | |
v v
create Session verify token
| |
v v
yield to handler yield User
|
handler runs
|
v
teardown in reverse:
session.close()
release lock Hands-on example
A typical four-layer setup: settings, session, repository, service.
from functools import lru_cache
from fastapi import Depends, FastAPI, HTTPException
from sqlalchemy.orm import Session, sessionmaker
from sqlalchemy import create_engine
from pydantic_settings import BaseSettings
class Settings(BaseSettings):
database_url: str
@lru_cache
def get_settings() -> Settings:
return Settings()
def get_engine(settings: Settings = Depends(get_settings)):
return create_engine(settings.database_url)
def get_db(engine = Depends(get_engine)):
SessionLocal = sessionmaker(bind=engine)
db = SessionLocal()
try:
yield db
finally:
db.close()
class UserRepo:
def __init__(self, db: Session): self.db = db
def get(self, uid: str): ...
def get_user_repo(db: Session = Depends(get_db)) -> UserRepo:
return UserRepo(db)
class UserService:
def __init__(self, repo: UserRepo): self.repo = repo
def fetch(self, uid: str):
user = self.repo.get(uid)
if not user:
raise HTTPException(404, "user not found")
return user
def get_user_service(repo: UserRepo = Depends(get_user_repo)) -> UserService:
return UserService(repo)
app = FastAPI()
@app.get("/users/{uid}")
def read_user(uid: str, svc: UserService = Depends(get_user_service)):
return svc.fetch(uid)
Notice the layering. The route handler does not know about the database; it only knows about the service. The service does not know about SQLAlchemy; it only knows about the repository. Each layer is testable in isolation.
For testing, override any dependency:
from fastapi.testclient import TestClient
def fake_user_service():
svc = UserService(repo=FakeRepo())
return svc
app.dependency_overrides[get_user_service] = fake_user_service
client = TestClient(app)
dependency_overrides is a dict you mutate before tests run. The framework checks it on every resolution, so swaps are immediate.
Yield dependencies are how you manage anything with setup and teardown: sessions, locks, files, distributed traces. Code after yield runs even if the handler raised, which makes it the right place for cleanup.
def get_lock(name: str = "default"):
lock = redis.lock(name, blocking_timeout=5)
acquired = lock.acquire()
try:
yield lock
finally:
if acquired:
lock.release()
Common pitfalls
Forgetting that dependencies are cached per request. If you call get_db from two routes via include_router, you get one session, not two. That is usually what you want; surprises happen when you assume otherwise.
use_cache=False disables caching for a single dependency. Useful for nonces or audit timestamps that should be re-evaluated.
Async vs sync mismatch. FastAPI happily mixes them, but a sync dependency that blocks (e.g., requests.get) will stall the event loop. Use async clients or push blocking calls to a thread pool with run_in_threadpool.
Heavy work inside dependencies runs on every request. A dependency that opens a connection pool is fine; one that builds the pool on every call is a disaster. Make pool builders module-level singletons or wrap them with lru_cache.
Circular dependencies happen when two helpers depend on each other. The resolver will raise; refactor to a common upstream dependency.
Production tips
Type your dependencies. The return type drives autocomplete in handlers and lets type checkers verify the wiring. Use Annotated[Session, Depends(get_db)] for cleaner signatures in modern code.
from typing import Annotated
SessionDep = Annotated[Session, Depends(get_db)]
@app.get("/users/{uid}")
def read_user(uid: str, db: SessionDep): ...
Keep dependencies pure when possible. A function that just reads config and constructs an object is easy to test. One that performs network calls during construction is not.
Push authentication into a dependency. current_user: User = Depends(get_current_user) makes every protected route obvious by inspection.
Use Depends for feature flags too. Inject a FeatureSet and let routes ask if flags.beta_search. This is easier to reason about than scattered environment lookups.
Wrap-up
Depends is the wiring layer of a FastAPI app. Use it to compose settings, sessions, repositories, and services, with yield dependencies for setup/teardown. Override in tests instead of monkeypatching. Type everything, cache singletons at module scope, and respect the sync/async boundary. Once the layering is in place, the routes become two-line orchestrators and the rest of your code is plain Python.
Related articles
- FastAPI Advanced Dependency Injection Patterns in FastAPI
Master advanced FastAPI dependency injection with nested deps, class-based providers, yield dependencies, and shared state patterns.
- Backend FastAPI vs Django vs Flask: Python Web Frameworks Compared
Compare FastAPI, Django, and Flask for Python web development. Understand performance, features, and ecosystem differences to choose the right framework.
- Python Pydantic v2: Data Validation and Settings Management
Learn Pydantic v2 for data validation, serialization, and settings management in Python. Covers models, validators, computed fields, and BaseSettings.
- FastAPI Background Tasks and Celery Integration in FastAPI
Learn to run background tasks in FastAPI using built-in BackgroundTasks and scale with Celery for distributed job processing.