Dependency Injection (Depends)¶
Dependency Injection (DI) lets you declare what your endpoint needs (database, current user, config) — FastAPI automatically provides it. Shared logic becomes reusable, testable, and decoupled.
The simplest possible example¶
from fastapi import FastAPI, Depends
app = FastAPI()
def common_pagination(page: int = 1, size: int = 10):
return {"page": page, "size": size}
@app.get("/users")
def list_users(pagination = Depends(common_pagination)):
return {"pagination": pagination, "users": [...]}
@app.get("/products")
def list_products(pagination = Depends(common_pagination)):
return {"pagination": pagination, "products": [...]}
Now both endpoints get ?page=2&size=20 for free, with the same validation logic — no duplication.
How it executes¶
When /users?page=2 comes in:
- FastAPI sees
pagination = Depends(common_pagination). - It calls
common_pagination(page=2, size=10)with parsed query params. - It passes the return value into your endpoint as
pagination. - Your function runs.
That's it. Dependencies are just functions FastAPI calls before your endpoint.
Why use Depends at all?¶
| Concern | Without DI | With DI |
|---|---|---|
| Reuse the same logic in 10 endpoints | Copy-paste 10 times | Write once, Depends(...) everywhere |
| Test an endpoint | Hard — DB connection baked in | Easy — swap dependency in tests |
| "Current user" check | Repeat in every endpoint | One get_current_user dependency |
| DB session per request | Manual setup + teardown | One dependency handles open/close |
DB session — the classic example¶
# db.py
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
engine = create_engine("sqlite:///app.db", connect_args={"check_same_thread": False})
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
def get_db():
db = SessionLocal()
try:
yield db # yield (not return) — see below
finally:
db.close()
# main.py
from fastapi import FastAPI, Depends
from sqlalchemy.orm import Session
from db import get_db
app = FastAPI()
@app.get("/users/{user_id}")
def get_user(user_id: int, db: Session = Depends(get_db)):
return db.query(User).filter(User.id == user_id).first()
yield + finally — guarantees the DB session closes even if your endpoint raises an exception. That's the dependency-with-teardown pattern.
Authentication — get_current_user¶
from fastapi import FastAPI, Depends, HTTPException, Header
app = FastAPI()
def get_current_user(authorization: str = Header(...)):
if not authorization.startswith("Bearer "):
raise HTTPException(401, "missing bearer token")
token = authorization.split()[1]
user = decode_token(token) # imagine this exists
if not user:
raise HTTPException(401, "invalid token")
return user
@app.get("/me")
def me(user = Depends(get_current_user)):
return user
@app.get("/me/orders")
def my_orders(user = Depends(get_current_user)):
return {"user": user["name"], "orders": [...]}
Add Depends(get_current_user) to any endpoint to require login.
Dependency that has its own dependencies¶
Dependencies can depend on other dependencies — FastAPI resolves the chain.
def get_db():
...
def get_user_repository(db: Session = Depends(get_db)):
return UserRepository(db)
def get_current_user(
repo: UserRepository = Depends(get_user_repository),
token: str = Header(..., alias="authorization"),
):
user = repo.find_by_token(token)
if not user:
raise HTTPException(401)
return user
@app.get("/profile")
def profile(user = Depends(get_current_user)):
return user
When /profile is called:
- get_db runs → opens DB session.
- get_user_repository runs with that session.
- get_current_user runs with that repo + the token.
- Your endpoint runs with the resulting user.
- After the endpoint finishes, get_db's finally block closes the session.
All wired up automatically.
Class-based dependencies¶
For dependencies with multiple parameters, a class is often cleaner:
class Paginator:
def __init__(self, page: int = 1, size: int = 10, sort: str = "id"):
self.page = page
self.size = size
self.sort = sort
@property
def offset(self) -> int:
return (self.page - 1) * self.size
@app.get("/items")
def list_items(p: Paginator = Depends()):
return {"page": p.page, "offset": p.offset, "sort": p.sort}
You can omit Paginator inside Depends() — FastAPI infers it from the type hint.
Apply a dependency to many endpoints — dependencies= parameter¶
If a dependency runs just for side-effects (auth check, rate limit) and you don't need its return value, attach it to the path operation or the entire router:
from fastapi import FastAPI, Depends, APIRouter
def verify_token(authorization: str = Header(...)):
if not authorization.startswith("Bearer "):
raise HTTPException(401)
# Per-endpoint
@app.get("/admin", dependencies=[Depends(verify_token)])
def admin_page():
return {"message": "admin only"}
# Per-router
router = APIRouter(dependencies=[Depends(verify_token)])
@router.get("/secret")
def secret():
return {"hello": "world"}
App-level dependencies¶
For something that should run on every request:
Now every endpoint requires the bearer token. Useful for fully-authenticated APIs.
Dependency overrides — for tests¶
Tests can replace any dependency:
# test_app.py
from fastapi.testclient import TestClient
from main import app, get_db
def fake_db():
yield InMemoryDB()
app.dependency_overrides[get_db] = fake_db
client = TestClient(app)
r = client.get("/users/1")
assert r.status_code == 200
Run integration tests without a real database. Powerful.
Dependency with yield — startup, teardown, error handling¶
def get_db():
db = SessionLocal()
try:
yield db
except Exception:
db.rollback()
raise
finally:
db.close()
Useful pattern: open resource → yield → cleanup, rollback on error.
When NOT to use Depends¶
- For pure data classes — use Pydantic models.
- For a single endpoint's logic — just write it inline.
- For configuration that never changes — read from a global.
Putting it together — a realistic mini-app¶
from fastapi import FastAPI, Depends, HTTPException, Header
from pydantic import BaseModel
app = FastAPI()
# --- "DB" (fake)
USERS = {
"tok_alice": {"id": 1, "name": "Alice", "role": "admin"},
"tok_bob": {"id": 2, "name": "Bob", "role": "user"},
}
# --- Dependencies
def get_current_user(authorization: str = Header(...)):
if not authorization.startswith("Bearer "):
raise HTTPException(401, "missing bearer")
token = authorization.split()[1]
user = USERS.get(token)
if not user:
raise HTTPException(401, "invalid token")
return user
def require_admin(user: dict = Depends(get_current_user)):
if user["role"] != "admin":
raise HTTPException(403, "admin only")
return user
# --- Endpoints
@app.get("/me")
def me(user = Depends(get_current_user)):
return user
@app.get("/admin/dashboard")
def admin(user = Depends(require_admin)):
return {"hello_admin": user["name"]}
Hit:
curl -H "Authorization: Bearer tok_alice" http://127.0.0.1:8000/me
curl -H "Authorization: Bearer tok_alice" http://127.0.0.1:8000/admin/dashboard # ok
curl -H "Authorization: Bearer tok_bob" http://127.0.0.1:8000/admin/dashboard # 403
curl http://127.0.0.1:8000/admin/dashboard # 422 (missing header)
Common pitfalls¶
- ❗ Forgetting
Depends(...)—db: Session = get_db(noDepends) makes FastAPI thinkget_dbis a default value, not a dependency. Always wrap inDepends(...). - ❗ Heavy work in
__init__of class dependencies — runs on every request. Use lazy initialization or caching. - ❗ Mixing sync and async dependencies — works, but async deps can't be called from a sync
defendpoint. Match them. - ❗ Caching that's too aggressive —
Depends(use_cache=True)(the default) caches within a request. If you need fresh each call, useuse_cache=False. - ❗ Circular dependencies — splitting modules helps. If A depends on B and B on A, you have a design problem.
What's next¶
Practice¶
What does this print?
Expected: db_session
Use Depends() (not direct function call) so FastAPI can inject and override it
Expected: True
Quiz — Quick check¶
What you remember
Q1. What does Depends(get_db) do?
- Calls
get_db()to compute a value, injects it into the handler — and lets you override it for tests - Imports the function
- A type annotation
- Validates the function
Why: Dependency injection separates "what" (the handler logic) from "how" (where the DB comes from). For tests, override
get_dbwith a mock — no test database needed.
Q2. Why use yield instead of return in a dependency?
- Code AFTER yield runs after the request completes — useful for cleanup (closing DB sessions, releasing locks)
- Faster
- Required by FastAPI
- No difference
Why: Yield-based dependencies are like context managers. The yielded value is injected; code after yield is the teardown. Critical for resources that need cleanup.
Q3. Can a dependency depend on another dependency?
- Yes — dependencies can have their own
Depends()params, forming a graph - No, only one level
- Yes but only 2 levels
- Only with decorators
Why: FastAPI builds a dependency graph and resolves it for each request. Common pattern:
get_current_userdepends onget_db(to look up the user). Each level is automatically resolved in order.
Common doubts¶
When should I use a dependency vs a regular function call?
Use Depends when (a) the function needs to be overridable for tests, (b) it needs FastAPI's resolution (e.g., extracting headers/tokens), or © it manages a resource lifecycle (DB session, file handle). For pure utility functions, just call them directly.
How do I override dependencies in tests?
app.dependency_overrides[get_db] = lambda: mock_db. Test client now gets the mock instead of the real DB. Clean up with app.dependency_overrides.clear() after the test.
Are dependencies called once per request or once per app start?
Once per request. For singleton dependencies (e.g., a model that's expensive to load), construct it once at startup and inject it from a global — or use lifespan events to manage app-level state.