Intermediate
You have a FastAPI application with ten routes, and every single one needs to verify the user’s API key, open a database session, and validate a pagination parameter. So you copy the same three blocks of code into every route handler. It works. Then you need to change how authentication works, and now you have ten places to update — and you will miss at least one. This is exactly the problem that dependency injection solves, and FastAPI ships with one of the cleanest implementations in any Python framework.
FastAPI’s Depends() system lets you declare reusable components — functions that provide shared logic or resources — and inject them into your routes automatically. You write the logic once. FastAPI calls it for every request that needs it, passes the result to your route, and handles teardown if needed. No service locator pattern, no manual wiring, no global state. The dependency is declared right in the function signature where you can see it.
This article covers the full Depends() system from first principles: simple dependency functions, parameterized factories, nested dependencies, class-based dependencies, database session management with yield, and authentication. Every example is runnable standalone. By the end you will be able to structure a real FastAPI application with clean, testable, shared logic across all your routes.
FastAPI Dependency Injection: Quick Example
Here is the minimal working version. We create a dependency function that extracts and validates a query parameter, then inject it into a route using Depends():
# quick_depends.py
from fastapi import FastAPI, Depends, HTTPException
app = FastAPI()
def get_pagination(page: int = 1, page_size: int = 10) -> dict:
if page < 1:
raise HTTPException(status_code=400, detail="page must be >= 1")
if page_size not in (10, 25, 50):
raise HTTPException(status_code=400, detail="page_size must be 10, 25, or 50")
return {"offset": (page - 1) * page_size, "limit": page_size}
@app.get("/users")
def list_users(pagination: dict = Depends(get_pagination)):
return {"offset": pagination["offset"], "limit": pagination["limit"]}
@app.get("/articles")
def list_articles(pagination: dict = Depends(get_pagination)):
return {"offset": pagination["offset"], "limit": pagination["limit"]}
Request:
GET /users?page=2&page_size=25
Output:
{"offset": 25, "limit": 25}
The key line is pagination: dict = Depends(get_pagination). FastAPI sees the Depends() annotation, calls get_pagination with the request’s query parameters, and passes the result to the route function as pagination. Both /users and /articles share the exact same validation logic with zero repetition. If you need to add page_size=100 as an allowed value later, you change one function.
The sections below go deeper: how FastAPI resolves dependencies, how to make them configurable, how to nest them, and how to use them for the two most common real-world cases — database sessions and authentication.
What Is Dependency Injection and Why Use It?
Dependency injection is a design pattern where a component declares what it needs, and something else provides it. In FastAPI, your route function declares its dependencies in its signature, and FastAPI’s dependency injection system resolves and provides them at request time. The route never reaches out to get its own resources — they are delivered to it.
The alternative is the approach most beginners start with: global state, imported singletons, or repeated boilerplate. All of these work until they cause problems. Global database connections break under concurrent load. Repeated boilerplate creates bugs when you update one copy and miss another. Imported singletons make unit testing painful because you cannot swap them for fakes.
Here is a concrete before-and-after comparison. Without Depends(), every route that needs authentication looks like this:
# without_depends.py -- the painful way
from fastapi import FastAPI, Header, HTTPException
app = FastAPI()
VALID_API_KEYS = {"secret-key-1", "secret-key-2"}
@app.get("/users")
def list_users(x_api_key: str = Header(...)):
if x_api_key not in VALID_API_KEYS:
raise HTTPException(status_code=401, detail="Invalid API key")
# ... actual route logic
return {"users": []}
@app.get("/articles")
def list_articles(x_api_key: str = Header(...)):
if x_api_key not in VALID_API_KEYS:
raise HTTPException(status_code=401, detail="Invalid API key")
# ... actual route logic
return {"articles": []}
With Depends(), the auth check lives in one place and is injected wherever it is needed:
# with_depends.py -- the clean way
from fastapi import FastAPI, Depends, Header, HTTPException
app = FastAPI()
VALID_API_KEYS = {"secret-key-1", "secret-key-2"}
def require_api_key(x_api_key: str = Header(...)):
if x_api_key not in VALID_API_KEYS:
raise HTTPException(status_code=401, detail="Invalid API key")
@app.get("/users")
def list_users(_: None = Depends(require_api_key)):
return {"users": []}
@app.get("/articles")
def list_articles(_: None = Depends(require_api_key)):
return {"articles": []}
The _: None = Depends(require_api_key) pattern is the idiomatic way to declare a dependency whose return value you do not need — you just want its side effects (raising a 401 if the key is wrong). This makes the dependency visible in the function signature, which makes it easy to audit and test.
| Pattern | Without Depends() | With Depends() |
|---|---|---|
| Auth logic location | Copied into every route | One function, injected everywhere |
| Update auth logic | Update N routes | Update one dependency |
| Unit test a route | Must mock global state | Override dependency in test client |
| DB session cleanup | Manual try/finally in every route | yield dependency handles it |
| Nested dependencies | Manual parameter threading | FastAPI resolves automatically |
Your First Dependency Function
A FastAPI dependency is any callable — a function, a method, a class — that FastAPI can call at request time. If it is a regular function, FastAPI calls it and passes the result to your route. If it is an async function, FastAPI awaits it. The callable can declare its own parameters, including query params, headers, path params, body fields, or even other Depends() calls.
Here is a slightly more complete example showing how a dependency accesses both a header and a query parameter:
# first_dependency.py
from fastapi import FastAPI, Depends, Header, Query, HTTPException
app = FastAPI()
def common_params(
accept_language: str = Header(default="en"),
q: str = Query(default=""),
limit: int = Query(default=20, ge=1, le=100),
):
"""Shared query context injected into multiple routes."""
return {
"language": accept_language.split(",")[0].strip(),
"search": q.lower().strip(),
"limit": limit,
}
@app.get("/search/users")
def search_users(ctx: dict = Depends(common_params)):
return {
"results": f"Searching users for '{ctx['search']}' in {ctx['language']}",
"limit": ctx["limit"],
}
@app.get("/search/articles")
def search_articles(ctx: dict = Depends(common_params)):
return {
"results": f"Searching articles for '{ctx['search']}' in {ctx['language']}",
"limit": ctx["limit"],
}
Request:
GET /search/users?q=python&limit=5
Accept-Language: fr-FR,fr;q=0.9,en;q=0.8
Output:
{"results": "Searching users for 'python' in fr-FR", "limit": 5}
FastAPI inspects common_params‘s signature, extracts accept_language from the request header, and q and limit from the query string — exactly as it would for a route function. The dependency is not a special object; it is a plain Python function with FastAPI-compatible annotations. This is what makes dependencies composable: they use the same parameter system as routes, so they can have their own dependencies too.
Parameterized Dependencies
Sometimes you want the same dependency logic but with different configuration. For example, you might have routes that require admin privileges and others that only require basic auth. You do not want two separate dependency functions — you want one function that behaves differently based on a parameter.
The pattern is a factory function that returns a dependency:
# parameterized_depends.py
from fastapi import FastAPI, Depends, Header, HTTPException
app = FastAPI()
USER_ROLES = {
"token-admin": "admin",
"token-editor": "editor",
"token-viewer": "viewer",
}
def require_role(minimum_role: str):
"""Factory that returns a dependency requiring a minimum role."""
role_levels = {"viewer": 1, "editor": 2, "admin": 3}
def check_role(authorization: str = Header(...)):
token = authorization.removeprefix("Bearer ").strip()
role = USER_ROLES.get(token)
if role is None:
raise HTTPException(status_code=401, detail="Invalid token")
if role_levels.get(role, 0) < role_levels[minimum_role]:
raise HTTPException(
status_code=403,
detail=f"Requires {minimum_role} role, got {role}"
)
return role
return check_role
@app.get("/admin/settings")
def admin_settings(role: str = Depends(require_role("admin"))):
return {"message": f"Admin panel accessed by {role}"}
@app.get("/editor/posts")
def editor_posts(role: str = Depends(require_role("editor"))):
return {"message": f"Post editor accessed by {role}"}
@app.get("/viewer/feed")
def viewer_feed(role: str = Depends(require_role("viewer"))):
return {"message": f"Feed accessed by {role}"}
Request (admin token):
GET /admin/settings
Authorization: Bearer token-admin
Output:
{"message": "Admin panel accessed by admin"}
Request (viewer token trying admin route):
GET /admin/settings
Authorization: Bearer token-viewer
Output (403):
{"detail": "Requires admin role, got viewer"}
require_role("admin") returns the inner check_role function, which FastAPI then calls as a normal dependency. Each route gets a different instance of check_role with a different minimum_role captured in its closure. This pattern scales cleanly -- add a new role or a new enforcement level without touching any route code.
Nested Dependencies
Dependencies can themselves depend on other dependencies. FastAPI resolves the full dependency graph before calling your route, injecting each dependency's result into the next one. This lets you build composable layers: a low-level "get current user token" dependency feeds a higher-level "get current user object" dependency, which feeds your route.
# nested_depends.py
from fastapi import FastAPI, Depends, Header, HTTPException
from dataclasses import dataclass
app = FastAPI()
TOKENS = {
"bearer-abc123": {"user_id": 1, "name": "Alice"},
"bearer-xyz789": {"user_id": 2, "name": "Bob"},
}
SUBSCRIPTIONS = {
1: "pro",
2: "free",
}
# Layer 1: extract raw token from header
def get_raw_token(authorization: str = Header(...)) -> str:
if not authorization.startswith("Bearer "):
raise HTTPException(status_code=401, detail="Bearer token required")
return authorization[7:].strip()
# Layer 2: resolve token to a user (depends on Layer 1)
@dataclass
class CurrentUser:
user_id: int
name: str
subscription: str
def get_current_user(token: str = Depends(get_raw_token)) -> CurrentUser:
user_data = TOKENS.get(token)
if user_data is None:
raise HTTPException(status_code=401, detail="Invalid token")
subscription = SUBSCRIPTIONS.get(user_data["user_id"], "free")
return CurrentUser(
user_id=user_data["user_id"],
name=user_data["name"],
subscription=subscription,
)
# Layer 3: restrict to pro users (depends on Layer 2)
def require_pro(user: CurrentUser = Depends(get_current_user)) -> CurrentUser:
if user.subscription != "pro":
raise HTTPException(
status_code=403,
detail=f"Pro subscription required. {user.name} has '{user.subscription}'"
)
return user
# Routes use the highest-level dependency they need
@app.get("/profile")
def get_profile(user: CurrentUser = Depends(get_current_user)):
return {"user_id": user.user_id, "name": user.name, "plan": user.subscription}
@app.get("/export")
def export_data(user: CurrentUser = Depends(require_pro)):
return {"message": f"Exporting data for {user.name} (pro account)"}
Request:
GET /profile
Authorization: Bearer bearer-abc123
Output:
{"user_id": 1, "name": "Alice", "plan": "pro"}
FastAPI resolves this left-to-right: it calls get_raw_token first, passes the token to get_current_user, and passes the resulting CurrentUser to either the route or to require_pro depending on which endpoint is hit. If any layer raises an exception, FastAPI short-circuits and returns the error response without calling further dependencies or the route itself. You get clean, layered authorization with no duplicated logic.
Class-Based Dependencies
When a dependency needs to hold state or configuration, a class is cleaner than a closure. You instantiate the class with configuration and pass the instance to Depends(). FastAPI calls the instance using its __call__ method, which can declare its own parameters just like a function dependency.
# class_dependency.py
from fastapi import FastAPI, Depends, Query, HTTPException
app = FastAPI()
class PaginationChecker:
"""Configurable pagination dependency that enforces site-wide rules."""
def __init__(self, max_page_size: int = 50, default_page_size: int = 10):
self.max_page_size = max_page_size
self.default_page_size = default_page_size
def __call__(
self,
page: int = Query(default=1, ge=1),
page_size: int = Query(default=None),
) -> dict:
if page_size is None:
page_size = self.default_page_size
if page_size > self.max_page_size:
raise HTTPException(
status_code=422,
detail=f"page_size cannot exceed {self.max_page_size}"
)
return {
"offset": (page - 1) * page_size,
"limit": page_size,
}
# Strict pagination for admin routes (max 25 results)
admin_pagination = PaginationChecker(max_page_size=25, default_page_size=10)
# Relaxed pagination for internal/bulk endpoints (max 200 results)
bulk_pagination = PaginationChecker(max_page_size=200, default_page_size=50)
@app.get("/admin/users")
def list_admin_users(page: dict = Depends(admin_pagination)):
return {"offset": page["offset"], "limit": page["limit"], "source": "admin"}
@app.get("/internal/export")
def bulk_export(page: dict = Depends(bulk_pagination)):
return {"offset": page["offset"], "limit": page["limit"], "source": "bulk"}
Output (admin route, page 3, size 25):
{"offset": 50, "limit": 25, "source": "admin"}
The class instance is callable, so FastAPI treats it exactly like a function dependency. The advantage over closures is that the class is easier to subclass, inspect, and test. You can create different instances with different configurations, pass them around, mock them in tests, and log their attributes without unpacking a closure.
Database Sessions with yield Dependencies
The most common use of Depends() in production FastAPI applications is managing database sessions. The pattern requires a dependency that opens a session before the route runs, passes it in, and closes the session after the route returns -- including on errors. This is exactly what yield dependencies do.
A yield dependency runs in two phases: everything before the yield is setup, and everything after is teardown. FastAPI guarantees the teardown runs whether the route succeeded or raised an exception.
# db_session_depends.py
from fastapi import FastAPI, Depends, HTTPException
from contextlib import contextmanager
app = FastAPI()
# Simulated database -- in production this would be SQLAlchemy SessionLocal
class FakeSession:
def __init__(self, session_id: int):
self.session_id = session_id
self.closed = False
print(f"[DB] Session {session_id} opened")
def query(self, table: str, record_id: int):
if table == "users" and record_id == 1:
return {"id": 1, "name": "Alice", "email": "alice@example.com"}
return None
def close(self):
self.closed = True
print(f"[DB] Session {self.session_id} closed")
_session_counter = 0
def get_db():
"""yield dependency: opens a session, provides it, then closes it."""
global _session_counter
_session_counter += 1
db = FakeSession(_session_counter)
try:
yield db # route receives db here
finally:
db.close() # always runs, even if route raised an exception
@app.get("/users/{user_id}")
def get_user(user_id: int, db: FakeSession = Depends(get_db)):
user = db.query("users", user_id)
if user is None:
raise HTTPException(status_code=404, detail="User not found")
return user
@app.get("/users/{user_id}/name")
def get_user_name(user_id: int, db: FakeSession = Depends(get_db)):
user = db.query("users", user_id)
if user is None:
raise HTTPException(status_code=404, detail="User not found")
return {"name": user["name"]}
Request:
GET /users/1
Output:
{"id": 1, "name": "Alice", "email": "alice@example.com"}
Server console (both requests):
[DB] Session 1 opened
[DB] Session 1 closed
[DB] Session 2 opened
[DB] Session 2 closed
Each request gets its own session, properly opened before the route runs and closed when it finishes -- regardless of whether the route succeeded or raised a 404. The try/finally block is what guarantees cleanup. Without it, an exception in the route would skip the db.close() call, leaking the session. In a real SQLAlchemy app, you would replace FakeSession with a SessionLocal() call from your database setup module, but the pattern is identical.
Authentication with Depends
Authentication is where Depends() pays off most clearly in real applications. Instead of decoding a JWT in every route, you write a single get_current_user dependency and inject it wherever auth is needed. Here is a complete example using PyJWT for token verification:
# auth_depends.py
from fastapi import FastAPI, Depends, Header, HTTPException
import json
import base64
app = FastAPI()
# Simplified token format: base64(json) -- replace with PyJWT in production
# In production: pip install python-jose[cryptography] and use jose.jwt.decode()
def create_fake_token(user_id: int, username: str) -> str:
payload = json.dumps({"user_id": user_id, "username": username})
return base64.b64encode(payload.encode()).decode()
def decode_fake_token(token: str) -> dict:
try:
payload = json.loads(base64.b64decode(token).decode())
if "user_id" not in payload or "username" not in payload:
raise ValueError("Missing fields")
return payload
except Exception:
raise HTTPException(status_code=401, detail="Could not validate token")
# The auth dependency
def get_current_user(authorization: str = Header(...)) -> dict:
if not authorization.startswith("Bearer "):
raise HTTPException(status_code=401, detail="Bearer token required")
token = authorization[7:].strip()
return decode_fake_token(token)
# Protected route -- injects user automatically
@app.get("/me")
def read_me(user: dict = Depends(get_current_user)):
return {"user_id": user["user_id"], "username": user["username"]}
@app.get("/me/settings")
def read_settings(user: dict = Depends(get_current_user)):
return {
"username": user["username"],
"theme": "dark",
"notifications": True,
}
# Helper to create a test token (not a protected route)
@app.post("/dev/token")
def create_dev_token(user_id: int, username: str):
return {"token": create_fake_token(user_id, username)}
Create a token:
POST /dev/token?user_id=1&username=alice
{"token": "eyJ1c2VyX2lkIjogMSwgInVzZXJuYW1lIjogImFsaWNlIn0="}
Use the token:
GET /me
Authorization: Bearer eyJ1c2VyX2lkIjogMSwgInVzZXJuYW1lIjogImFsaWNlIn0=
{"user_id": 1, "username": "alice"}
The get_current_user dependency does all the heavy lifting: reading the header, stripping the bearer prefix, decoding the token, and raising the appropriate 401 if anything goes wrong. Every protected route just declares user: dict = Depends(get_current_user) and gets the decoded user dict automatically. To switch from this simplified token format to real PyJWT tokens, you change one dependency function. Every route stays the same.
Router-Level and App-Level Dependencies
So far we have declared dependencies on individual routes. FastAPI also lets you attach dependencies to an entire APIRouter or the whole FastAPI app. Any dependency attached this way runs for every route in that router or app, without needing to appear in each function signature.
# router_depends.py
from fastapi import FastAPI, Depends, Header, HTTPException, APIRouter
app = FastAPI()
VALID_KEYS = {"internal-key-abc", "internal-key-xyz"}
def verify_internal_key(x_internal_key: str = Header(...)):
if x_internal_key not in VALID_KEYS:
raise HTTPException(status_code=403, detail="Invalid internal key")
# All routes in this router require x_internal_key header
internal_router = APIRouter(
prefix="/internal",
dependencies=[Depends(verify_internal_key)],
)
@internal_router.get("/metrics")
def get_metrics():
return {"requests_today": 1204, "errors": 3}
@internal_router.get("/health")
def health_check():
return {"status": "ok", "db": "connected"}
app.include_router(internal_router)
# Public route -- no key required
@app.get("/ping")
def ping():
return {"pong": True}
Request without key:
GET /internal/metrics
{"detail": "Field required"} (422) or {"detail": "Invalid internal key"} (403)
Request with key:
GET /internal/metrics
X-Internal-Key: internal-key-abc
{"requests_today": 1204, "errors": 3}
The dependencies=[Depends(verify_internal_key)] argument on the router applies the check to every route under /internal without modifying any route function. You can stack multiple dependencies in the list. The same pattern works at the app level: FastAPI(dependencies=[Depends(log_all_requests)]) runs that dependency on every single request, which is useful for request logging or rate limiting middleware.
Real-Life Example: Protected API with Shared DB and Rate Limiting
Here is a realistic example that ties together nested dependencies, a yield database session, authentication, and a simple rate limiter -- all wired together with Depends():
# full_api.py
from fastapi import FastAPI, Depends, Header, HTTPException
from collections import defaultdict
from datetime import datetime, timedelta
from dataclasses import dataclass
app = FastAPI()
# --- Fake data stores ---
USERS_DB = {
"token-alice": {"user_id": 1, "username": "alice", "role": "admin"},
"token-bob": {"user_id": 2, "username": "bob", "role": "viewer"},
}
NOTES_DB = {
1: {"id": 1, "owner_id": 1, "text": "Deploy on Friday"},
2: {"id": 2, "owner_id": 1, "text": "Review PRs before noon"},
3: {"id": 3, "owner_id": 2, "text": "Read the FastAPI docs"},
}
# --- Rate limiter: max 5 requests per minute per user ---
_rate_data: dict = defaultdict(list)
def check_rate_limit(user: dict) -> None:
user_id = user["user_id"]
now = datetime.utcnow()
window = now - timedelta(minutes=1)
_rate_data[user_id] = [t for t in _rate_data[user_id] if t > window]
if len(_rate_data[user_id]) >= 5:
raise HTTPException(
status_code=429,
detail=f"Rate limit exceeded: {user['username']} > 5 req/min"
)
_rate_data[user_id].append(now)
# --- Dependencies ---
class FakeDB:
"""Simulates a database session."""
def get_notes_for_user(self, user_id: int) -> list:
return [n for n in NOTES_DB.values() if n["owner_id"] == user_id]
def get_note(self, note_id: int) -> dict | None:
return NOTES_DB.get(note_id)
def get_db():
db = FakeDB()
try:
yield db
finally:
pass # In real code: db.close()
def get_current_user(authorization: str = Header(...)) -> dict:
token = authorization.removeprefix("Bearer ").strip()
user = USERS_DB.get(token)
if user is None:
raise HTTPException(status_code=401, detail="Invalid token")
return user
def get_rate_limited_user(
user: dict = Depends(get_current_user),
) -> dict:
check_rate_limit(user)
return user
# --- Routes ---
@app.get("/notes")
def list_notes(
user: dict = Depends(get_rate_limited_user),
db: FakeDB = Depends(get_db),
):
notes = db.get_notes_for_user(user["user_id"])
return {"user": user["username"], "notes": notes}
@app.get("/notes/{note_id}")
def get_note(
note_id: int,
user: dict = Depends(get_rate_limited_user),
db: FakeDB = Depends(get_db),
):
note = db.get_note(note_id)
if note is None:
raise HTTPException(status_code=404, detail="Note not found")
if note["owner_id"] != user["user_id"] and user["role"] != "admin":
raise HTTPException(status_code=403, detail="Access denied")
return note
Request:
GET /notes
Authorization: Bearer token-alice
Output:
{
"user": "alice",
"notes": [
{"id": 1, "owner_id": 1, "text": "Deploy on Friday"},
{"id": 2, "owner_id": 1, "text": "Review PRs before noon"}
]
}
This is a complete, production-ready dependency chain. get_rate_limited_user calls get_current_user as a nested dependency, so FastAPI resolves auth first, then checks the rate limit, then enters the route. If any step fails, the chain short-circuits. The database session opens for each request and closes after the route returns. To add a new protected endpoint, you declare two dependencies and write the route logic -- authentication, rate limiting, and DB cleanup are handled automatically.
Testing Routes with Depends()
One of the most important benefits of Depends() is testability. FastAPI's TestClient accepts a dependency_overrides dictionary that lets you swap any dependency with a stub for testing -- without modifying a single line of production code.
# test_api.py
from fastapi.testclient import TestClient
from full_api import app, get_current_user, get_db, FakeDB
# Override the auth dependency with a fake that always returns Alice
def override_get_current_user():
return {"user_id": 1, "username": "alice", "role": "admin"}
# Override the DB with a test-specific FakeDB
class TestDB(FakeDB):
def get_notes_for_user(self, user_id: int) -> list:
return [{"id": 99, "owner_id": user_id, "text": "Test note"}]
def override_get_db():
yield TestDB()
app.dependency_overrides[get_current_user] = override_get_current_user
app.dependency_overrides[get_db] = override_get_db
client = TestClient(app)
def test_list_notes():
response = client.get("/notes", headers={"Authorization": "Bearer anything"})
assert response.status_code == 200
data = response.json()
assert data["user"] == "alice"
assert len(data["notes"]) == 1
assert data["notes"][0]["text"] == "Test note"
Test output:
PASSED test_api.py::test_list_notes
The production route never changed. The test supplies its own auth and DB by overriding two entries in app.dependency_overrides. After tests run, clear the overrides with app.dependency_overrides.clear() to avoid affecting other tests. This is the correct way to test FastAPI applications -- not by mocking global state or patching imports, but by replacing dependencies cleanly at the application boundary.
Frequently Asked Questions
What is the difference between Depends() and middleware?
Middleware runs for every request before any routing happens and has access to the raw request and response objects. Depends() runs as part of route resolution and has access to parsed, typed parameters (query params, headers, body fields) along with FastAPI's dependency injection system. Use middleware for cross-cutting concerns that need the raw request or response (CORS, request logging, timing). Use Depends() for business logic like authentication, database sessions, and parameter validation that benefits from FastAPI's type system and testability.
Can a dependency run code after the route returns?
Yes -- use a yield dependency. Everything before the yield runs before the route; everything after runs after the route returns. FastAPI guarantees the post-yield code runs even if the route raised an exception, as long as the exception handling is in a try/finally block around the yield. This is the correct pattern for database sessions, file handles, or any resource that needs cleanup after the request.
Does FastAPI call the same dependency multiple times if two routes both depend on it?
Within a single request, FastAPI caches dependency results by default. If your route depends on get_current_user and your route also depends on a require_pro that itself depends on get_current_user, FastAPI calls get_current_user only once and reuses the result. You can disable this cache with Depends(get_current_user, use_cache=False) if you explicitly need the dependency called multiple times, but this is rarely the right answer.
Can I declare a dependency on a class directly, without __call__?
Yes. When you pass a class to Depends(), FastAPI calls the class's __init__ method and passes the resulting instance to your route. This is different from a class with __call__: the class itself is the dependency, and FastAPI instantiates it per request. The class-with-__call__ pattern (where you create one instance and reuse it) is for when you want configurable, shared behavior. The class-as-dependency pattern (where FastAPI instantiates it per request) is for when the class itself needs per-request parameters injected into its __init__.
How do I share a dependency result between the route and a background task?
You cannot inject dependencies into background tasks the same way you do into routes -- background tasks run after the response is sent, and by then the dependency's teardown code may have already closed the database session. The correct pattern is to pass the data you need into the background task explicitly: call the database inside the background task function using a fresh session (opened and closed within the task), rather than passing the route's session into the task. Each background task should manage its own resources independently.
What happens if a dependency raises an exception?
FastAPI intercepts the exception, sends the appropriate error response (matching the status code of any HTTPException), and does not call the route handler. If a yield dependency has already yielded before an exception occurs in a later dependency or in the route, FastAPI still runs the post-yield cleanup code for that dependency. This is why the try/finally pattern is critical: it guarantees cleanup runs regardless of where the error originated in the dependency chain.
Conclusion
FastAPI's Depends() system turns shared logic from a copy-paste problem into a composable, testable, and maintainable architecture. We covered simple dependency functions that validate query parameters, parameterized factories that configure behavior via closures, nested dependencies that build authentication layers, class-based dependencies for stateful configuration, yield dependencies that manage database sessions with guaranteed cleanup, and router-level dependencies that apply to all routes in a group without touching individual function signatures.
The real-life example at the end shows how all these pieces fit together: auth, rate limiting, and database access in a clean dependency chain that adds fewer than ten lines to each route. The testing section shows the payoff -- swapping any dependency for a stub without touching production code. That is the value proposition of dependency injection, and FastAPI's implementation is one of the best in any framework.
The next step is to apply this pattern to your own codebase. Pick one piece of repeated logic -- auth, pagination, or a database session -- and extract it into a dependency. Then inject it with Depends(). You will immediately see how the route functions shrink and how tests become easier to write. For the full reference on dependency injection, see the official FastAPI documentation on dependencies.