FastAPI Dependency Injection: Auth, DB Sessions, Routers
1 · The lesson
readDependency injection has a reputation for being heavyweight enterprise-Java ceremony. FastAPI's version is almost the opposite — a dependency is a plain Python function. You declare param: Annotated[SomeType, Depends(some_fn)] in your endpoint, and FastAPI calls some_fn() for you, caches the result for the request, and injects it. Auth check, DB session, settings, paginator, feature flag, current-user lookup — same mechanism, three lines each.
Run locally with pip install 'fastapi[standard]' pydantic-settings and fastapi dev main.py. Expected output shown in comments.
The reason this matters more than the mechanics suggest — dependencies are the seam where production code and tests differ. Override one function in a test and the entire request path uses the fake. That is what makes FastAPI testable in a way that Flask requires plugins to match.
1. The Minimum Dependency
A dependency is a function. You name it as a parameter via Depends:
from typing import Annotated from fastapi import Depends, FastAPI app = FastAPI() def get_greeting() -> str: return "Hello" @app.get("/") def home(greeting: Annotated[str, Depends(get_greeting)]): return {"message": greeting + ", world"} # GET / -> {"message":"Hello, world"}
Annotated[str, Depends(get_greeting)] reads as "this parameter is a str, sourced by calling get_greeting". FastAPI inspects the signature at import time, resolves the call graph, and runs get_greeting() before each request that needs it. The function can take its own dependencies, including request data — they get resolved first, recursively.
The old form greeting: str = Depends(get_greeting) still works. The Annotated form is the documented current style — it composes better with type checkers and stacks naturally with Path, Query, Header.
2. Function vs Class Dependencies
Two flavours, same protocol.
Function — the default, what you'll use 90% of the time:
def common_pagination(skip: int = 0, limit: int = 10): return {"skip": skip, "limit": limit} @app.get("/items/") def list_items(p: Annotated[dict, Depends(common_pagination)]): return p # GET /items/?skip=20&limit=5 -> {"skip":20,"limit":5}
setup added so this can run · defines app, Annotated, Depends
# Lightweight mock for objects whose attributes/methods aren't critical class _AutoMock: def __init__(self, name='mock'): self._name = name def __getattr__(self, k): return _AutoMock(self._name + '.' + k) def __call__(self, *a, **kw): print('-> ' + self._name + '() called') return _AutoMock(self._name + '()') def __repr__(self): return '<mock ' + self._name + '>' def __str__(self): return '<mock ' + self._name + '>' def __bool__(self): return True def __iter__(self): return iter([]) def __len__(self): return 0 def __getitem__(self, k): return _AutoMock(self._name + '[...]') def __setitem__(self, k, v): pass def __enter__(self): return self def __exit__(self, *a): return False async def __aenter__(self): return self async def __aexit__(self, *a): return False def __add__(self, o): return self def __radd__(self, o): return self def __sub__(self, o): return self def __mul__(self, o): return self def __rmul__(self, o): return self def __truediv__(self, o): return self def __eq__(self, o): return isinstance(o, _AutoMock) def __hash__(self): return hash(self._name) def __lt__(self, o): return True def __le__(self, o): return True def __gt__(self, o): return False def __ge__(self, o): return False def __mro_entries__(self, bases): return (object,) app = _AutoMock('app') Annotated = _AutoMock('Annotated') def Depends(*_a, **_kw): print('-> Depends() called') return _AutoMock('Depends()')
The dependency's parameters become query parameters of the route — exactly as if they were declared on the route itself. Reuse one definition across a dozen endpoints.
Class — when the dependency holds state worth reusing:
class Paginator: def __init__(self, skip: int = 0, limit: int = 10): self.skip = skip self.limit = max(1, min(limit, 100)) def slice(self, items: list) -> list: return items[self.skip : self.skip + self.limit] @app.get("/items/") def list_items(p: Annotated[Paginator, Depends()]): all_items = ["a", "b", "c", "d", "e"] return p.slice(all_items)
setup added so this can run · defines app, Annotated, Depends
# Lightweight mock for objects whose attributes/methods aren't critical class _AutoMock: def __init__(self, name='mock'): self._name = name def __getattr__(self, k): return _AutoMock(self._name + '.' + k) def __call__(self, *a, **kw): print('-> ' + self._name + '() called') return _AutoMock(self._name + '()') def __repr__(self): return '<mock ' + self._name + '>' def __str__(self): return '<mock ' + self._name + '>' def __bool__(self): return True def __iter__(self): return iter([]) def __len__(self): return 0 def __getitem__(self, k): return _AutoMock(self._name + '[...]') def __setitem__(self, k, v): pass def __enter__(self): return self def __exit__(self, *a): return False async def __aenter__(self): return self async def __aexit__(self, *a): return False def __add__(self, o): return self def __radd__(self, o): return self def __sub__(self, o): return self def __mul__(self, o): return self def __rmul__(self, o): return self def __truediv__(self, o): return self def __eq__(self, o): return isinstance(o, _AutoMock) def __hash__(self): return hash(self._name) def __lt__(self, o): return True def __le__(self, o): return True def __gt__(self, o): return False def __ge__(self, o): return False def __mro_entries__(self, bases): return (object,) app = _AutoMock('app') Annotated = _AutoMock('Annotated') def Depends(*_a, **_kw): print('-> Depends() called') return _AutoMock('Depends()')
Depends() with no argument falls back to the parameter's type annotation as the callable — Depends(Paginator). You get a fresh instance per request, and you can call methods on it from the handler. Use a class when the dependency has behaviour beyond producing a value.
3. Sub-Dependencies — Dependencies That Depend on Dependencies
FastAPI resolves the whole dependency graph. A dependency can declare its own Depends(...) parameters — they get filled in just like the route's.
def query_token(token: str = "") -> str: return token def verify_token(token: Annotated[str, Depends(query_token)]) -> str: if token != "secret": raise HTTPException(401, "Invalid token") return token @app.get("/protected") def protected(token: Annotated[str, Depends(verify_token)]): return {"token": token} # GET /protected?token=secret -> {"token":"secret"} # GET /protected?token=nope -> 401
setup added so this can run · defines app, Annotated, HTTPException, Depends
# Lightweight mock for objects whose attributes/methods aren't critical class _AutoMock: def __init__(self, name='mock'): self._name = name def __getattr__(self, k): return _AutoMock(self._name + '.' + k) def __call__(self, *a, **kw): print('-> ' + self._name + '() called') return _AutoMock(self._name + '()') def __repr__(self): return '<mock ' + self._name + '>' def __str__(self): return '<mock ' + self._name + '>' def __bool__(self): return True def __iter__(self): return iter([]) def __len__(self): return 0 def __getitem__(self, k): return _AutoMock(self._name + '[...]') def __setitem__(self, k, v): pass def __enter__(self): return self def __exit__(self, *a): return False async def __aenter__(self): return self async def __aexit__(self, *a): return False def __add__(self, o): return self def __radd__(self, o): return self def __sub__(self, o): return self def __mul__(self, o): return self def __rmul__(self, o): return self def __truediv__(self, o): return self def __eq__(self, o): return isinstance(o, _AutoMock) def __hash__(self): return hash(self._name) def __lt__(self, o): return True def __le__(self, o): return True def __gt__(self, o): return False def __ge__(self, o): return False def __mro_entries__(self, bases): return (object,) app = _AutoMock('app') Annotated = _AutoMock('Annotated') def HTTPException(*_a, **_kw): print('-> HTTPException() called') return _AutoMock('HTTPException()') def Depends(*_a, **_kw): print('-> Depends() called') return _AutoMock('Depends()')
verify_token depends on query_token, which extracts the value from a query parameter. The route just asks for verify_token. FastAPI builds the chain.
Within one request, a dependency is cached by default — if two siblings depend on get_db, both receive the same session. Disable with Depends(get_db, use_cache=False) when you genuinely need a fresh value per use.
4. yield Dependencies — Setup + Teardown
The most useful pattern. A dependency that needs cleanup uses yield like a pytest fixture:
def get_db(): db = SessionLocal() try: yield db # value injected into the route finally: db.close() # runs after the response is sent @app.get("/users/{user_id}") def get_user(user_id: int, db: Annotated[Session, Depends(get_db)]): return db.get(User, user_id)
setup added so this can run · defines SessionLocal, User, app, Annotated, Session, Depends
# Lightweight mock for objects whose attributes/methods aren't critical class _AutoMock: def __init__(self, name='mock'): self._name = name def __getattr__(self, k): return _AutoMock(self._name + '.' + k) def __call__(self, *a, **kw): print('-> ' + self._name + '() called') return _AutoMock(self._name + '()') def __repr__(self): return '<mock ' + self._name + '>' def __str__(self): return '<mock ' + self._name + '>' def __bool__(self): return True def __iter__(self): return iter([]) def __len__(self): return 0 def __getitem__(self, k): return _AutoMock(self._name + '[...]') def __setitem__(self, k, v): pass def __enter__(self): return self def __exit__(self, *a): return False async def __aenter__(self): return self async def __aexit__(self, *a): return False def __add__(self, o): return self def __radd__(self, o): return self def __sub__(self, o): return self def __mul__(self, o): return self def __rmul__(self, o): return self def __truediv__(self, o): return self def __eq__(self, o): return isinstance(o, _AutoMock) def __hash__(self): return hash(self._name) def __lt__(self, o): return True def __le__(self, o): return True def __gt__(self, o): return False def __ge__(self, o): return False def __mro_entries__(self, bases): return (object,) def SessionLocal(*_a, **_kw): print('-> SessionLocal() called') return _AutoMock('SessionLocal()') User = _AutoMock('User') app = _AutoMock('app') Annotated = _AutoMock('Annotated') Session = _AutoMock('Session') def Depends(*_a, **_kw): print('-> Depends() called') return _AutoMock('Depends()')
Code before yield runs at the start of the request; code after runs after the response has been sent (or after an exception). The try/finally ensures the session closes even if the handler raises. This is the canonical FastAPI database pattern — one session per request, opened on demand, always closed.
The same pattern handles transactions, file handles, temporary directories, distributed locks. For async dependencies, async def + yield works identically (you'll need async with on the resource if it supports it).
5. Auth as a Dependency
Authentication is a perfect dependency — every protected route needs it, the logic is identical across routes, and you want one place to update when the policy changes.
import os from fastapi import Header def get_current_user(authorization: Annotated[str | None, Header()] = None): if authorization is None or not authorization.startswith("Bearer "): raise HTTPException(401, "Missing bearer token") token = authorization.removeprefix("Bearer ") if token != os.environ["API_TOKEN"]: raise HTTPException(401, "Invalid token") return {"username": "surya", "scopes": ["read", "write"]} @app.get("/me") def me(user: Annotated[dict, Depends(get_current_user)]): return user @app.get("/admin") def admin(user: Annotated[dict, Depends(get_current_user)]): if "admin" not in user["scopes"]: raise HTTPException(403, "Admin only") return {"secret": "..."}
setup added so this can run · defines app, Annotated, HTTPException, Depends
import os # noqa: F401 os.environ.setdefault("API_TOKEN", "example-api-token") # Lightweight mock for objects whose attributes/methods aren't critical class _AutoMock: def __init__(self, name='mock'): self._name = name def __getattr__(self, k): return _AutoMock(self._name + '.' + k) def __call__(self, *a, **kw): print('-> ' + self._name + '() called') return _AutoMock(self._name + '()') def __repr__(self): return '<mock ' + self._name + '>' def __str__(self): return '<mock ' + self._name + '>' def __bool__(self): return True def __iter__(self): return iter([]) def __len__(self): return 0 def __getitem__(self, k): return _AutoMock(self._name + '[...]') def __setitem__(self, k, v): pass def __enter__(self): return self def __exit__(self, *a): return False async def __aenter__(self): return self async def __aexit__(self, *a): return False def __add__(self, o): return self def __radd__(self, o): return self def __sub__(self, o): return self def __mul__(self, o): return self def __rmul__(self, o): return self def __truediv__(self, o): return self def __eq__(self, o): return isinstance(o, _AutoMock) def __hash__(self): return hash(self._name) def __lt__(self, o): return True def __le__(self, o): return True def __gt__(self, o): return False def __ge__(self, o): return False def __mro_entries__(self, bases): return (object,) app = _AutoMock('app') Annotated = _AutoMock('Annotated') def HTTPException(*_a, **_kw): print('-> HTTPException() called') return _AutoMock('HTTPException()') def Depends(*_a, **_kw): print('-> Depends() called') return _AutoMock('Depends()')
Both routes have the same dependency. The dependency reads the header, validates the token, and returns a user object — or raises 401, which short-circuits the route entirely. The handler never runs on an invalid request.
Real apps use OAuth2PasswordBearer, JWT validation, or session lookups — see auth-jwt for the full pattern. The shape doesn't change: a dependency function that returns the user or raises.
6. Global and Per-Router Dependencies
Some dependencies should apply to every route — request logging, a tenant header check, a global rate limiter. Attach them at the app level:
app = FastAPI(dependencies=[Depends(verify_tenant_header)])
setup added so this can run · defines FastAPI, Depends, verify_tenant_header
# Lightweight mock for objects whose attributes/methods aren't critical class _AutoMock: def __init__(self, name='mock'): self._name = name def __getattr__(self, k): return _AutoMock(self._name + '.' + k) def __call__(self, *a, **kw): print('-> ' + self._name + '() called') return _AutoMock(self._name + '()') def __repr__(self): return '<mock ' + self._name + '>' def __str__(self): return '<mock ' + self._name + '>' def __bool__(self): return True def __iter__(self): return iter([]) def __len__(self): return 0 def __getitem__(self, k): return _AutoMock(self._name + '[...]') def __setitem__(self, k, v): pass def __enter__(self): return self def __exit__(self, *a): return False async def __aenter__(self): return self async def __aexit__(self, *a): return False def __add__(self, o): return self def __radd__(self, o): return self def __sub__(self, o): return self def __mul__(self, o): return self def __rmul__(self, o): return self def __truediv__(self, o): return self def __eq__(self, o): return isinstance(o, _AutoMock) def __hash__(self): return hash(self._name) def __lt__(self, o): return True def __le__(self, o): return True def __gt__(self, o): return False def __ge__(self, o): return False def __mro_entries__(self, bases): return (object,) def FastAPI(*_a, **_kw): print('-> FastAPI() called') return _AutoMock('FastAPI()') def Depends(*_a, **_kw): print('-> Depends() called') return _AutoMock('Depends()') verify_tenant_header = _AutoMock('verify_tenant_header')
Every route inherits the dependency, with no per-route declaration. The dependency runs (and can raise) before any handler.
The same trick at the router level scopes a dependency to a subset of routes — every route under /admin, for instance:
admin_router = APIRouter( prefix="/admin", tags=["admin"], dependencies=[Depends(require_admin)], )
setup added so this can run · defines APIRouter, Depends, require_admin
# Lightweight mock for objects whose attributes/methods aren't critical class _AutoMock: def __init__(self, name='mock'): self._name = name def __getattr__(self, k): return _AutoMock(self._name + '.' + k) def __call__(self, *a, **kw): print('-> ' + self._name + '() called') return _AutoMock(self._name + '()') def __repr__(self): return '<mock ' + self._name + '>' def __str__(self): return '<mock ' + self._name + '>' def __bool__(self): return True def __iter__(self): return iter([]) def __len__(self): return 0 def __getitem__(self, k): return _AutoMock(self._name + '[...]') def __setitem__(self, k, v): pass def __enter__(self): return self def __exit__(self, *a): return False async def __aenter__(self): return self async def __aexit__(self, *a): return False def __add__(self, o): return self def __radd__(self, o): return self def __sub__(self, o): return self def __mul__(self, o): return self def __rmul__(self, o): return self def __truediv__(self, o): return self def __eq__(self, o): return isinstance(o, _AutoMock) def __hash__(self): return hash(self._name) def __lt__(self, o): return True def __le__(self, o): return True def __gt__(self, o): return False def __ge__(self, o): return False def __mro_entries__(self, bases): return (object,) def APIRouter(*_a, **_kw): print('-> APIRouter() called') return _AutoMock('APIRouter()') def Depends(*_a, **_kw): print('-> Depends() called') return _AutoMock('Depends()') require_admin = _AutoMock('require_admin')
Dependencies declared on the route, the router, and the app all run — global first, then router, then route. The return values from dependencies=[...] are not injected into the handler signature (they're for side-effects: auth checks, logging). Inject only the ones the handler actually needs as parameters.
7. Pydantic Settings — Config as a Dependency
Hardcoding the database URL is the start of a long debugging session. Pydantic Settings reads from environment variables, validates types, and gives you a single typed config object:
pip install pydantic-settings
from functools import lru_cache from pydantic_settings import BaseSettings, SettingsConfigDict class Settings(BaseSettings): model_config = SettingsConfigDict(env_file=".env") database_url: str api_token: str debug: bool = False @lru_cache def get_settings() -> Settings: return Settings() # reads env vars + .env file @app.get("/info") def info(settings: Annotated[Settings, Depends(get_settings)]): return {"debug": settings.debug}
setup added so this can run · defines app, Annotated, Depends
# Lightweight mock for objects whose attributes/methods aren't critical class _AutoMock: def __init__(self, name='mock'): self._name = name def __getattr__(self, k): return _AutoMock(self._name + '.' + k) def __call__(self, *a, **kw): print('-> ' + self._name + '() called') return _AutoMock(self._name + '()') def __repr__(self): return '<mock ' + self._name + '>' def __str__(self): return '<mock ' + self._name + '>' def __bool__(self): return True def __iter__(self): return iter([]) def __len__(self): return 0 def __getitem__(self, k): return _AutoMock(self._name + '[...]') def __setitem__(self, k, v): pass def __enter__(self): return self def __exit__(self, *a): return False async def __aenter__(self): return self async def __aexit__(self, *a): return False def __add__(self, o): return self def __radd__(self, o): return self def __sub__(self, o): return self def __mul__(self, o): return self def __rmul__(self, o): return self def __truediv__(self, o): return self def __eq__(self, o): return isinstance(o, _AutoMock) def __hash__(self): return hash(self._name) def __lt__(self, o): return True def __le__(self, o): return True def __gt__(self, o): return False def __ge__(self, o): return False def __mro_entries__(self, bases): return (object,) app = _AutoMock('app') Annotated = _AutoMock('Annotated') def Depends(*_a, **_kw): print('-> Depends() called') return _AutoMock('Depends()')
Settings() raises at startup if a required env var is missing. @lru_cache makes get_settings a singleton — every dependency call returns the same instance for the lifetime of the process. Override in tests with app.dependency_overrides[get_settings] = lambda: Settings(database_url="sqlite:///:memory:", api_token="test").
The pattern composes — a get_db dependency takes Settings as a sub-dependency to find its DSN:
def get_db(settings: Annotated[Settings, Depends(get_settings)]): engine = create_engine(settings.database_url) db = Session(engine) try: yield db finally: db.close()
setup added so this can run · defines create_engine, Session, Annotated, Settings, Depends, get_settings
# Lightweight mock for objects whose attributes/methods aren't critical class _AutoMock: def __init__(self, name='mock'): self._name = name def __getattr__(self, k): return _AutoMock(self._name + '.' + k) def __call__(self, *a, **kw): print('-> ' + self._name + '() called') return _AutoMock(self._name + '()') def __repr__(self): return '<mock ' + self._name + '>' def __str__(self): return '<mock ' + self._name + '>' def __bool__(self): return True def __iter__(self): return iter([]) def __len__(self): return 0 def __getitem__(self, k): return _AutoMock(self._name + '[...]') def __setitem__(self, k, v): pass def __enter__(self): return self def __exit__(self, *a): return False async def __aenter__(self): return self async def __aexit__(self, *a): return False def __add__(self, o): return self def __radd__(self, o): return self def __sub__(self, o): return self def __mul__(self, o): return self def __rmul__(self, o): return self def __truediv__(self, o): return self def __eq__(self, o): return isinstance(o, _AutoMock) def __hash__(self): return hash(self._name) def __lt__(self, o): return True def __le__(self, o): return True def __gt__(self, o): return False def __ge__(self, o): return False def __mro_entries__(self, bases): return (object,) def create_engine(*_a, **_kw): print('-> create_engine() called') return _AutoMock('create_engine()') def Session(*_a, **_kw): print('-> Session() called') return _AutoMock('Session()') Annotated = _AutoMock('Annotated') Settings = _AutoMock('Settings') def Depends(*_a, **_kw): print('-> Depends() called') return _AutoMock('Depends()') get_settings = _AutoMock('get_settings')
8. Routers — FastAPI's "Blueprints"
A single main.py works for a webhook. A real app has dozens of endpoints in clear domains — users, products, orders, admin. APIRouter is the unit of organisation:
# routers/users.py from fastapi import APIRouter, Depends from typing import Annotated router = APIRouter( prefix="/users", tags=["users"], dependencies=[Depends(get_current_user)], responses={404: {"description": "Not found"}}, ) @router.get("/{user_id}") def get_user(user_id: int, db = Depends(get_db)): return db.get(User, user_id)
setup added so this can run · defines get_db, User, get_current_user
# Lightweight mock for objects whose attributes/methods aren't critical class _AutoMock: def __init__(self, name='mock'): self._name = name def __getattr__(self, k): return _AutoMock(self._name + '.' + k) def __call__(self, *a, **kw): print('-> ' + self._name + '() called') return _AutoMock(self._name + '()') def __repr__(self): return '<mock ' + self._name + '>' def __str__(self): return '<mock ' + self._name + '>' def __bool__(self): return True def __iter__(self): return iter([]) def __len__(self): return 0 def __getitem__(self, k): return _AutoMock(self._name + '[...]') def __setitem__(self, k, v): pass def __enter__(self): return self def __exit__(self, *a): return False async def __aenter__(self): return self async def __aexit__(self, *a): return False def __add__(self, o): return self def __radd__(self, o): return self def __sub__(self, o): return self def __mul__(self, o): return self def __rmul__(self, o): return self def __truediv__(self, o): return self def __eq__(self, o): return isinstance(o, _AutoMock) def __hash__(self): return hash(self._name) def __lt__(self, o): return True def __le__(self, o): return True def __gt__(self, o): return False def __ge__(self, o): return False def __mro_entries__(self, bases): return (object,) get_db = _AutoMock('get_db') User = _AutoMock('User') get_current_user = _AutoMock('get_current_user')
# main.py from fastapi import FastAPI from routers import users, products, orders app = FastAPI() app.include_router(users.router) app.include_router(products.router) app.include_router(orders.router)
The router declares its own prefix, tags (drives Swagger UI grouping), default dependencies, and shared response schemas. The main app stays a thin assembly file. This is the FastAPI equivalent of Flask blueprints and Django apps — same idea, lighter syntax.
9. Project Structure for Medium Apps
A project layout that scales from one endpoint to fifty without rewrites:
app/
main.py # FastAPI() instance, include_routers, lifespan
config.py # Settings, get_settings
dependencies/
__init__.py
auth.py # get_current_user, require_admin
db.py # get_db, get_engine
models/ # SQLAlchemy ORM models
user.py
product.py
schemas/ # Pydantic request/response models
user.py
product.py
services/ # business logic, no framework imports
user_service.py
product_service.py
routers/
users.py
products.py
tests/
conftest.py
test_users.py
test_products.pyThree rules:
services/knows nothing about FastAPI. Pure Python that takes the inputs it needs and returns plain objects. Easy to unit-test, easy to reuse if you ever swap frameworks.routers/is glue. Marshal the request, call a service, return the response. No SQL queries, no auth checks (those are dependencies), no business logic.schemas/is for wire shapes,models/is for storage shapes. Don't conflate them; the two diverge as the app grows.
10. Middleware — Cross-Cutting Concerns
Middleware wraps every request — before the dependency chain runs and after the response is built. Use it for things that are truly global and don't fit the dependency model: CORS, gzip, request IDs, latency metrics.
from fastapi.middleware.cors import CORSMiddleware from fastapi.middleware.gzip import GZipMiddleware import time app.add_middleware( CORSMiddleware, allow_origins=["https://example.com"], allow_methods=["GET", "POST"], allow_headers=["*"], ) app.add_middleware(GZipMiddleware, minimum_size=1000) @app.middleware("http") async def add_process_time_header(request, call_next): start = time.perf_counter() response = await call_next(request) response.headers["X-Process-Time"] = f"{time.perf_counter() - start:.4f}" return response
setup added so this can run · defines app
# Lightweight mock for objects whose attributes/methods aren't critical class _AutoMock: def __init__(self, name='mock'): self._name = name def __getattr__(self, k): return _AutoMock(self._name + '.' + k) def __call__(self, *a, **kw): print('-> ' + self._name + '() called') return _AutoMock(self._name + '()') def __repr__(self): return '<mock ' + self._name + '>' def __str__(self): return '<mock ' + self._name + '>' def __bool__(self): return True def __iter__(self): return iter([]) def __len__(self): return 0 def __getitem__(self, k): return _AutoMock(self._name + '[...]') def __setitem__(self, k, v): pass def __enter__(self): return self def __exit__(self, *a): return False async def __aenter__(self): return self async def __aexit__(self, *a): return False def __add__(self, o): return self def __radd__(self, o): return self def __sub__(self, o): return self def __mul__(self, o): return self def __rmul__(self, o): return self def __truediv__(self, o): return self def __eq__(self, o): return isinstance(o, _AutoMock) def __hash__(self): return hash(self._name) def __lt__(self, o): return True def __le__(self, o): return True def __gt__(self, o): return False def __ge__(self, o): return False def __mro_entries__(self, bases): return (object,) app = _AutoMock('app')
Middleware order matters — add_middleware is called outermost-first. The CORS middleware should run last (outermost) so its headers survive everything else. Auth, business-rule checks, and per-route resources belong in dependencies, not middleware — middleware is too coarse-grained to express "this applies to these routes but not those".
11. Testing Override — The Killer Reason
Dependencies aren't just an organisation tool — they're the seam where tests differ from production. app.dependency_overrides is a dict; put your fake in, the real dependency is replaced for the duration:
# tests/test_users.py from fastapi.testclient import TestClient from app.main import app from app.dependencies.db import get_db def fake_db(): return InMemoryDB() # returns a fake, no setup app.dependency_overrides[get_db] = fake_db client = TestClient(app) r = client.get("/users/1") assert r.status_code == 200 app.dependency_overrides.clear() # IMPORTANT — undo for the next test
setup added so this can run · defines InMemoryDB
# Lightweight mock for objects whose attributes/methods aren't critical class _AutoMock: def __init__(self, name='mock'): self._name = name def __getattr__(self, k): return _AutoMock(self._name + '.' + k) def __call__(self, *a, **kw): print('-> ' + self._name + '() called') return _AutoMock(self._name + '()') def __repr__(self): return '<mock ' + self._name + '>' def __str__(self): return '<mock ' + self._name + '>' def __bool__(self): return True def __iter__(self): return iter([]) def __len__(self): return 0 def __getitem__(self, k): return _AutoMock(self._name + '[...]') def __setitem__(self, k, v): pass def __enter__(self): return self def __exit__(self, *a): return False async def __aenter__(self): return self async def __aexit__(self, *a): return False def __add__(self, o): return self def __radd__(self, o): return self def __sub__(self, o): return self def __mul__(self, o): return self def __rmul__(self, o): return self def __truediv__(self, o): return self def __eq__(self, o): return isinstance(o, _AutoMock) def __hash__(self): return hash(self._name) def __lt__(self, o): return True def __le__(self, o): return True def __gt__(self, o): return False def __ge__(self, o): return False def __mro_entries__(self, bases): return (object,) def InMemoryDB(*_a, **_kw): print('-> InMemoryDB() called') return _AutoMock('InMemoryDB()')
Now every route that depends on get_db (directly or transitively) uses the fake. No mocking, no monkeypatching at import paths, no rewriting handlers. The same pattern overrides auth (get_current_user → lambda: {"username": "test"}), settings, and any external client.
This is the single biggest reason to express anything that varies between environments — DB, auth, config, queue clients — as a dependency rather than a module-level singleton. The full testing playbook is in fastapi-testing.
Common Mistakes
1. Module-level DB session / client
db = SessionLocal() # opened at import time, never closed client = httpx.AsyncClient() # leaks on shutdown, can't override
setup added so this can run · defines SessionLocal, httpx
# Lightweight mock for objects whose attributes/methods aren't critical class _AutoMock: def __init__(self, name='mock'): self._name = name def __getattr__(self, k): return _AutoMock(self._name + '.' + k) def __call__(self, *a, **kw): print('-> ' + self._name + '() called') return _AutoMock(self._name + '()') def __repr__(self): return '<mock ' + self._name + '>' def __str__(self): return '<mock ' + self._name + '>' def __bool__(self): return True def __iter__(self): return iter([]) def __len__(self): return 0 def __getitem__(self, k): return _AutoMock(self._name + '[...]') def __setitem__(self, k, v): pass def __enter__(self): return self def __exit__(self, *a): return False async def __aenter__(self): return self async def __aexit__(self, *a): return False def __add__(self, o): return self def __radd__(self, o): return self def __sub__(self, o): return self def __mul__(self, o): return self def __rmul__(self, o): return self def __truediv__(self, o): return self def __eq__(self, o): return isinstance(o, _AutoMock) def __hash__(self): return hash(self._name) def __lt__(self, o): return True def __le__(self, o): return True def __gt__(self, o): return False def __ge__(self, o): return False def __mro_entries__(self, bases): return (object,) def SessionLocal(*_a, **_kw): print('-> SessionLocal() called') return _AutoMock('SessionLocal()') httpx = _AutoMock('httpx')
No per-request lifecycle, no test override, no lifespan cleanup. Move to a yield dependency for per-request resources, or open in lifespan for shared pools that the dependency hands out.
2. Forgetting to clear app.dependency_overrides
def test_a(): app.dependency_overrides[get_db] = fake_db # ... test ... # forgot to clear -> test_b sees the fake too
setup added so this can run · defines fake_db, get_db, app
# Lightweight mock for objects whose attributes/methods aren't critical class _AutoMock: def __init__(self, name='mock'): self._name = name def __getattr__(self, k): return _AutoMock(self._name + '.' + k) def __call__(self, *a, **kw): print('-> ' + self._name + '() called') return _AutoMock(self._name + '()') def __repr__(self): return '<mock ' + self._name + '>' def __str__(self): return '<mock ' + self._name + '>' def __bool__(self): return True def __iter__(self): return iter([]) def __len__(self): return 0 def __getitem__(self, k): return _AutoMock(self._name + '[...]') def __setitem__(self, k, v): pass def __enter__(self): return self def __exit__(self, *a): return False async def __aenter__(self): return self async def __aexit__(self, *a): return False def __add__(self, o): return self def __radd__(self, o): return self def __sub__(self, o): return self def __mul__(self, o): return self def __rmul__(self, o): return self def __truediv__(self, o): return self def __eq__(self, o): return isinstance(o, _AutoMock) def __hash__(self): return hash(self._name) def __lt__(self, o): return True def __le__(self, o): return True def __gt__(self, o): return False def __ge__(self, o): return False def __mro_entries__(self, bases): return (object,) fake_db = _AutoMock('fake_db') get_db = _AutoMock('get_db') app = _AutoMock('app')
Use a pytest fixture with yield to set and clear, or call app.dependency_overrides.clear() in a finally. The next test silently runs against your fake, hides a bug, and you debug for an hour.
3. Business logic in dependencies
A dependency that fetches the user is fine. A dependency that runs your entire order-checkout flow because "it returns the order receipt" is misuse. Dependencies produce values or guard access. Services do the work. The route is the glue.
4. Circular dep imports
routers/users.py imports from dependencies/auth.py, which imports from routers/users.py. The cycle. Put dependencies in their own package, with no upward imports — they should depend on config, models, and the standard library, nothing else.
5. Using Depends(fn) for things that aren't dependencies
Custom utility functions that you call inside the handler don't need Depends. Just call them. Depends is for things that:
- Should be injected per-request rather than imported,
- Want their own params turned into query/header/body parameters,
- Need
yieldteardown, - Should be overridable in tests.
If none of those apply, it's a regular function call.
🎯 Your Turn — Bearer Auth Dependency + Test Override
Build a get_current_user dependency that validates a bearer token against an env var. Protect two routes with it. Demonstrate how a test would override the dependency to skip the auth check.
Requirements:
get_current_user(authorization: Annotated[str, Header()])reads theAuthorizationheader.- If it doesn't start with
Beareror the token doesn't matchos.environ["API_TOKEN"], raiseHTTPException(401, ..., headers={"WWW-Authenticate": "Bearer"}). - Return a dict
{"username": "surya"}on success. - Two protected routes:
GET /meandGET /private. - Show the test override that replaces the dependency with a function returning a fake user.
# main.py import os from typing import Annotated from fastapi import Depends, FastAPI, Header, HTTPException, status app = FastAPI() # TODO 1: define get_current_user(...) — read header, validate, return user dict # TODO 2: GET /me using Depends(get_current_user) # TODO 3: GET /private using Depends(get_current_user) # TODO 4: in a comment block, show the TestClient override pattern
Hint 1 — Reading headers
authorization: Annotated[str | None, Header()] = None in the dependency signature pulls the Authorization header. FastAPI auto-converts the header name Authorization to the snake_case parameter authorization. Hyphens become underscores. Default None so the dependency itself decides how to handle the missing case.
Hint 2 — Override the dep, don't monkey-patch the env
For the test pattern, define a no-op replacementdef fake_user(): return {"username": "test"}, then app.dependency_overrides[get_current_user] = fake_user. Now every route that depends on get_current_user sees the fake. Reset with app.dependency_overrides.clear() after the test.
Show full solution
# main.py import os from typing import Annotated from fastapi import Depends, FastAPI, Header, HTTPException, status app = FastAPI() def get_current_user( authorization: Annotated[str | None, Header()] = None, ) -> dict: """Validate a Bearer token against API_TOKEN. Raise 401 on failure.""" if authorization is None or not authorization.startswith("Bearer "): raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="Missing Bearer token", headers={"WWW-Authenticate": "Bearer"}, ) token = authorization.removeprefix("Bearer ") expected = os.environ.get("API_TOKEN") if expected is None or token != expected: raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid token", headers={"WWW-Authenticate": "Bearer"}, ) return {"username": "surya", "scopes": ["read", "write"]} @app.get("/me") def me(user: Annotated[dict, Depends(get_current_user)]): return user @app.get("/private") def private(user: Annotated[dict, Depends(get_current_user)]): return {"secret": "for your eyes only", "user": user["username"]} # ─── tests/test_auth.py ────────────────────────────────────────────── # from fastapi.testclient import TestClient # from main import app, get_current_user # # def fake_user(): # return {"username": "test-user", "scopes": ["read"]} # # def test_me_with_override(): # app.dependency_overrides[get_current_user] = fake_user # try: # client = TestClient(app) # r = client.get("/me") # no Authorization header — still passes # assert r.status_code == 200 # assert r.json()["username"] == "test-user" # finally: # app.dependency_overrides.clear() # # def test_me_unauthorized(): # client = TestClient(app) # no override -> real dep runs # r = client.get("/me") # assert r.status_code == 401 # assert r.headers["WWW-Authenticate"] == "Bearer"
setup added so this can run · defines
import os # noqa: F401 os.environ.setdefault("API_TOKEN", "example-api-token")
Two routes share one dependency. The dependency holds the entire auth contract — header shape, token comparison, error response, WWW-Authenticate header — in one place. Adding a third protected route is one more Depends(get_current_user).
The test override replaces the entire auth check with a function that just returns a user dict. No header to send, no env var to set, no monkeypatching of os.environ. Every protected route in the suite gets the fake automatically. The try/finally is the discipline you need so the override doesn't leak into the next test — wrap it in a pytest fixture for repeated use:
@pytest.fixture def authed_client(): app.dependency_overrides[get_current_user] = lambda: {"username": "test"} yield TestClient(app) app.dependency_overrides.clear()
setup added so this can run · defines pytest, get_current_user, app, TestClient
# Lightweight mock for objects whose attributes/methods aren't critical class _AutoMock: def __init__(self, name='mock'): self._name = name def __getattr__(self, k): return _AutoMock(self._name + '.' + k) def __call__(self, *a, **kw): print('-> ' + self._name + '() called') return _AutoMock(self._name + '()') def __repr__(self): return '<mock ' + self._name + '>' def __str__(self): return '<mock ' + self._name + '>' def __bool__(self): return True def __iter__(self): return iter([]) def __len__(self): return 0 def __getitem__(self, k): return _AutoMock(self._name + '[...]') def __setitem__(self, k, v): pass def __enter__(self): return self def __exit__(self, *a): return False async def __aenter__(self): return self async def __aexit__(self, *a): return False def __add__(self, o): return self def __radd__(self, o): return self def __sub__(self, o): return self def __mul__(self, o): return self def __rmul__(self, o): return self def __truediv__(self, o): return self def __eq__(self, o): return isinstance(o, _AutoMock) def __hash__(self): return hash(self._name) def __lt__(self, o): return True def __le__(self, o): return True def __gt__(self, o): return False def __ge__(self, o): return False def __mro_entries__(self, bases): return (object,) pytest = _AutoMock('pytest') get_current_user = _AutoMock('get_current_user') app = _AutoMock('app') def TestClient(*_a, **_kw): print('-> TestClient() called') return _AutoMock('TestClient()')
This is the pattern the next lesson — fastapi-testing — builds out for the full test suite.
What You Learned
- A dependency is a function injected via
Annotated[T, Depends(fn)]. FastAPI resolves the whole graph per request. - Class dependencies (
Depends(Paginator)) work too — useful when the value has behaviour, not just data. - Sub-dependencies compose; FastAPI caches each one per request unless you pass
use_cache=False. yielddependencies give you setup + teardown — the canonical DB-session pattern.- Auth as a dependency is the standard FastAPI pattern — header check + token validation + user object, applied to every protected route.
- App-level and router-level
dependencies=[...]apply to every route in scope. Return values from these are not injected into the handler. - Pydantic Settings +
get_settingsgives you a typed, env-driven config object — wrap with@lru_cachefor singleton semantics. APIRouterorganises routes by domain — prefix, tags, shared deps, shared response schemas.- Project structure — separate
services/(pure logic),routers/(glue),schemas/(wire),models/(storage),dependencies/(the seam). - Middleware for truly global cross-cutting concerns. Auth and per-resource work belong in dependencies.
app.dependency_overridesis the killer test feature — replace any dep with a fake, no monkeypatching. Always clear it between tests.
Next: FastAPI Testing — TestClient, async tests with httpx.AsyncClient, fixtures for isolated state, dependency overrides done properly, and the testing habits that make a FastAPI service trustworthy in CI.