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
- Django Django REST Framework vs FastAPI Compared
A practical comparison of DRF and FastAPI: performance, ORM, validation, async, and how to choose for a new Python service.
- FastAPI FastAPI Authentication with JWT
Implement JWT-based authentication in FastAPI with OAuth2 password flow, secure token signing, and a reusable get_current_user dependency.
- FastAPI FastAPI CORS: A Practical Tutorial
Configure CORS in FastAPI without security holes: how the browser preflight works, which origins and headers to allow, credentials and cookies, and the most common misconfigurations to avoid.
- FastAPI FastAPI Deployment with Uvicorn and Gunicorn
Deploy FastAPI to production with Gunicorn managing Uvicorn workers. Cover process counts, timeouts, and health checks.