What Dependency Injection Means in FastAPI
Dependency Injection (DI) is a pattern where your route handlers declare what they need (a database session, settings, an authenticated user, parsed query parameters), and FastAPI provides those values automatically. In FastAPI, DI is implemented primarily with Depends.
Instead of manually creating resources inside every endpoint (and duplicating the same code), you extract that logic into dependency functions. This keeps route handlers focused on business logic and makes shared concerns consistent across your API.
Why DI is useful
- Reuse: One implementation of database session creation, pagination parsing, or auth checks can be shared across many routes.
- Consistency: The same validation and defaults apply everywhere.
- Composability: Dependencies can depend on other dependencies.
- Testability: You can override dependencies in tests to swap real services for fakes.
How Depends Works
A dependency is usually a function (sync or async) whose parameters can themselves be dependencies. FastAPI resolves the dependency graph per request, caches results when appropriate, and injects the returned value into your endpoint.
from fastapi import Depends, FastAPIapp = FastAPI()def get_config() -> dict: return {"feature_x": True}@app.get("/status")def status(config: dict = Depends(get_config)): return {"ok": True, "feature_x": config["feature_x"]}FastAPI calls get_config() and passes its return value into status() as config.
Dependency Scopes and Lifetimes
Most dependencies are resolved once per request. Within a single request, FastAPI caches dependency results so that if the same dependency is required multiple times (directly or indirectly), it is executed only once.
Continue in our app.
You can listen to the audiobook with the screen off, receive a free certificate for this course, and also have access to 5,000 other free online courses.
Or continue reading below...Download the app
Per-request caching
If get_db() is used by multiple dependencies and the endpoint, FastAPI will typically create one session per request and reuse it.
Yield dependencies for setup/teardown
When you need cleanup (closing a DB session, releasing a connection), use yield. Code after yield runs after the response is produced.
from typing import Generatordef get_resource() -> Generator[str, None, None]: resource = "opened" try: yield resource finally: resource = "closed"Request vs application-level state
DI is not a replacement for application startup configuration. Use DI for values that are request-scoped (user, request-specific parsing, DB session) or for abstracting access to app-scoped objects (settings, clients) in a testable way.
Composing Dependencies (Dependencies Depending on Dependencies)
Dependencies compose naturally: a dependency can declare its own dependencies. This is a powerful pattern for authentication and authorization, where “current user” depends on “token extraction,” which depends on “settings,” etc.
from fastapi import Dependsdef get_settings() -> dict: return {"auth_enabled": True}def get_token(settings: dict = Depends(get_settings)) -> str | None: if not settings["auth_enabled"]: return None # stub: in real code you'd read Authorization header return "fake-token"def get_current_user(token: str | None = Depends(get_token)) -> dict: if token is None: return {"id": "anonymous", "role": "guest"} return {"id": "user_123", "role": "member"}Now any endpoint can request current_user without knowing how tokens are obtained.
Reusable Dependency Patterns
1) Database session dependency (get_db)
A common pattern is to create one database session per request and ensure it is closed even if an error occurs.
from typing import Generator# Assume SessionLocal is your session factory (e.g., from SQLAlchemy)def get_db() -> Generator["Session", None, None]: db = SessionLocal() try: yield db finally: db.close()Usage in a router:
from fastapi import APIRouter, Dependsrouter = APIRouter(prefix="/items", tags=["items"])@router.get("/")def list_items(db = Depends(get_db)): # db is a request-scoped session return []2) Configuration/settings dependency
Even if settings are application-scoped, injecting them via a dependency makes it easy to override in tests.
from dataclasses import dataclass@dataclass(frozen=True)class Settings: page_size_default: int = 20 page_size_max: int = 100def get_settings() -> Settings: return Settings()Usage:
def some_endpoint(settings: Settings = Depends(get_settings)): return {"default": settings.page_size_default}3) Common query parsing: pagination parameters
Instead of repeating page/limit parsing in every list endpoint, centralize it. You can return a small object (dict, dataclass, or Pydantic model) representing the parsed parameters.
from dataclasses import dataclassfrom fastapi import Depends, Query@dataclass(frozen=True)class Pagination: offset: int limit: intdef pagination_params( page: int = Query(1, ge=1), page_size: int | None = Query(None, ge=1), settings: Settings = Depends(get_settings),) -> Pagination: size = page_size or settings.page_size_default size = min(size, settings.page_size_max) return Pagination(offset=(page - 1) * size, limit=size)Usage:
@router.get("/")def list_items( db = Depends(get_db), pagination: Pagination = Depends(pagination_params),): # Use pagination.offset and pagination.limit in queries return {"offset": pagination.offset, "limit": pagination.limit, "items": []}4) Authentication check: current_user stub
You often want endpoints to receive a user object that is guaranteed to exist (or raise an error). Here we keep it as a stub to focus on DI patterns.
from dataclasses import dataclassfrom fastapi import Depends, HTTPException, status@dataclass(frozen=True)class User: id: str role: strdef current_user() -> User: # stub: replace with real auth later user = User(id="user_123", role="member") if not user: raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Not authenticated") return userUsage:
@router.post("/")def create_item( db = Depends(get_db), user: User = Depends(current_user),): return {"created_by": user.id}5) Authorization as a dependency (role check)
Authorization is a great use case for dependency composition: a “require admin” dependency can depend on current_user.
def require_role(required: str): def _checker(user: User = Depends(current_user)) -> User: if user.role != required: raise HTTPException(status_code=403, detail="Forbidden") return user return _checker@router.delete("/{item_id}")def delete_item( item_id: str, db = Depends(get_db), user: User = Depends(require_role("admin")),): return {"deleted": item_id, "by": user.id}Dependency Injection and Testability
DI improves testability because route handlers don’t create concrete resources directly. Instead, they depend on abstract providers that can be replaced during tests.
Overriding dependencies
FastAPI allows overriding dependencies via app.dependency_overrides. This lets you replace get_db with a fake session, or current_user with a deterministic user.
from fastapi.testclient import TestClientdef fake_get_db(): class FakeDB: def close(self): pass db = FakeDB() try: yield db finally: db.close()def fake_current_user() -> User: return User(id="test_user", role="admin")app.dependency_overrides[get_db] = fake_get_dbapp.dependency_overrides[current_user] = fake_current_userclient = TestClient(app)def test_delete_item_as_admin(): r = client.delete("/items/abc") assert r.status_code == 200This approach avoids hitting real databases or implementing real auth in unit tests.
Step-by-Step Refactor: Replace Duplicated Code in Routers with Dependencies
Imagine you have multiple routers with repeated code: creating a DB session, parsing pagination, and checking authentication. The refactor goal is to move those repeated blocks into dependencies and keep endpoints small.
Step 1: Identify duplication
Typical repeated patterns you might see:
- Creating/closing DB sessions in every endpoint
- Parsing
page/page_sizerepeatedly - Copy/pasting user checks or role checks
Step 2: Create a dependencies.py module
Centralize shared dependencies so routers import them from one place.
# app/dependencies.pyfrom dataclasses import dataclassfrom typing import Generatorfrom fastapi import Depends, HTTPException, Queryfrom .settings import Settings, get_settingsfrom .db import SessionLocal@dataclass(frozen=True)class Pagination: offset: int limit: intdef get_db() -> Generator["Session", None, None]: db = SessionLocal() try: yield db finally: db.close()def pagination_params( page: int = Query(1, ge=1), page_size: int | None = Query(None, ge=1), settings: Settings = Depends(get_settings),) -> Pagination: size = page_size or settings.page_size_default size = min(size, settings.page_size_max) return Pagination(offset=(page - 1) * size, limit=size)@dataclass(frozen=True)class User: id: str role: strdef current_user() -> User: user = User(id="user_123", role="member") if not user: raise HTTPException(status_code=401, detail="Not authenticated") return userdef require_role(required: str): def _checker(user: User = Depends(current_user)) -> User: if user.role != required: raise HTTPException(status_code=403, detail="Forbidden") return user return _checkerStep 3: Refactor routers to use dependencies
Before refactor (duplicated patterns inside endpoints):
# app/routers/items.pyfrom fastapi import APIRouter, HTTPException, Queryfrom ..db import SessionLocalrouter = APIRouter(prefix="/items", tags=["items"])@router.get("/")def list_items(page: int = Query(1, ge=1), page_size: int = Query(20, ge=1)): db = SessionLocal() try: offset = (page - 1) * page_size limit = page_size return {"offset": offset, "limit": limit, "items": []} finally: db.close()@router.post("/")def create_item(): db = SessionLocal() try: # repeated auth stub user = {"id": "user_123"} if not user: raise HTTPException(status_code=401, detail="Not authenticated") return {"created_by": user["id"]} finally: db.close()After refactor (dependencies injected):
# app/routers/items.pyfrom fastapi import APIRouter, Dependsfrom ..dependencies import Pagination, User, current_user, get_db, pagination_paramsrouter = APIRouter(prefix="/items", tags=["items"])@router.get("/")def list_items( db = Depends(get_db), pagination: Pagination = Depends(pagination_params),): return {"offset": pagination.offset, "limit": pagination.limit, "items": []}@router.post("/")def create_item( db = Depends(get_db), user: User = Depends(current_user),): return {"created_by": user.id}Step 4: Apply the same refactor across other routers
Once dependencies exist, other routers can adopt them with minimal changes. For example, an admin-only router can use require_role("admin") everywhere without repeating checks.
# app/routers/admin.pyfrom fastapi import APIRouter, Dependsfrom ..dependencies import User, get_db, require_rolerouter = APIRouter(prefix="/admin", tags=["admin"])@router.get("/metrics")def metrics( db = Depends(get_db), user: User = Depends(require_role("admin")),): return {"viewer": user.id, "metrics": {"requests": 123}}Step 5: Keep route handlers thin
A useful rule of thumb is: if multiple endpoints need the same pre-work (open DB, parse query params, load user, enforce role), that pre-work should be a dependency. Endpoints should read like a small orchestration layer that calls your domain logic with already-prepared inputs.
| Concern | Where it belongs | Result |
|---|---|---|
| DB session lifecycle | get_db yield dependency | One session per request, always closed |
| Pagination parsing | pagination_params | Consistent defaults and limits |
| Authentication | current_user | Endpoints receive a user object |
| Authorization | require_role | Role checks are reusable and centralized |
| Testing | dependency_overrides | Swap real dependencies for fakes |