DI Patterns — Auth Chains, Router Guards & Test Overrides
AdvancedReal apps stack dependencies into auth chains (token → user → role check), guard whole routers with one line, and swap any node via dependency_overrides in tests — the architecture is the dependency graph.
Overview
The previous two chapters gave you the parts; this one is how production apps assemble them. Authentication becomes a chain: get_token reads the header, get_current_user decodes and loads, require_role(...) closes over a role and rejects with 403 — endpoints then declare their guard level in the signature, self-documenting who may call them. Router-level dependencies apply a guard to every route in a module (the whole /admin router requires admin, enforced in exactly one line), eliminating the classic bug of the one forgotten unprotected endpoint. And app.dependency_overrides is the payoff for wiring everything through Depends: tests replace the real DB session or the real auth with fakes per-node, no monkeypatching, no test backdoors in production code.
The Auth Chain + Role Factories
Each link does one job. require_role is a dependency factory — a function returning a dependency closed over its arguments — the same factory trick as class dependencies, in function form.
from typing import Annotated
from fastapi import Depends, FastAPI, HTTPException, Header
app = FastAPI()
FAKE_USERS = {"token-asha": {"name": "Asha", "role": "student"},
"token-tpo": {"name": "Rane", "role": "tpo"}}
def get_token(authorization: str | None = Header(default=None)) -> str:
if not authorization or not authorization.startswith("Bearer "):
raise HTTPException(401, "not authenticated") # 401: who are you?
return authorization.removeprefix("Bearer ")
def get_current_user(token: Annotated[str, Depends(get_token)]) -> dict:
user = FAKE_USERS.get(token) # JWT decode + DB load in real life
if not user:
raise HTTPException(401, "invalid or expired token")
return user
CurrentUser = Annotated[dict, Depends(get_current_user)]
def require_role(*roles: str): # ← dependency FACTORY
def checker(user: CurrentUser) -> dict:
if user["role"] not in roles:
raise HTTPException(403, "insufficient permissions") # 403: not you
return user
return checker
@app.get("/profile")
def profile(user: CurrentUser): # any logged-in user
return user
@app.post("/drives")
def create_drive(user: Annotated[dict, Depends(require_role("tpo"))]):
return {"created_by": user["name"]}
# token-asha on POST /drives → 403 · no token → 401
# The signature IS the access-control documentation.Router-Level Guards and dependency_overrides
Guard a whole router in its constructor — impossible to forget on route #23. In tests, override any node in the graph: fake user, sqlite session, permissive limiter — production code stays clean.
from fastapi import APIRouter, Depends, FastAPI
from fastapi.testclient import TestClient
# ── every route in this router requires the tpo role — ONE line ──
admin = APIRouter(
prefix="/admin",
tags=["Admin"],
dependencies=[Depends(require_role("tpo"))], # applies to all routes below
)
@admin.get("/students") # protected
def all_students(): return []
@admin.delete("/drives/{id}") # protected — nobody can forget
def drop_drive(id: int): return {"deleted": id}
app = FastAPI()
app.include_router(admin)
# app-wide version: FastAPI(dependencies=[Depends(verify_api_key)])
# ── tests: swap graph nodes, touch nothing else ──
def fake_tpo():
return {"name": "TestTPO", "role": "tpo"}
app.dependency_overrides[get_current_user] = fake_tpo # auth bypassed cleanly
client = TestClient(app)
assert client.get("/admin/students").status_code == 200 # no token needed
app.dependency_overrides.clear() # ALWAYS clear (pytest fixture)
assert client.get("/admin/students").status_code == 401
# The same trick swaps get_db → sqlite session (Testing chapters).
# Because require_role depends on get_current_user, overriding the
# user node reconfigures every guard built on top of it.Key Points to Remember
- 1Auth = dependency chain: token → user → role; endpoints declare their guard
- 2require_role(*roles) is a dependency factory — closures carrying config
- 3401 = not authenticated, 403 = authenticated but not permitted
- 4Router/app-level dependencies guard everything; dependency_overrides swaps nodes in tests
Interview Questions
Sign in to ask AriaDesign role-based access for student/TPO/admin in FastAPI — show the dependency chain.
How do you guarantee no endpoint in /admin ships unprotected?
How does dependency_overrides work, and why is it better than monkeypatching auth in tests?
Ask Aria about DI Patterns — Auth Chains, Router Guards & Test Overrides
Your personal AI tutor — ask anything about this concept
Revision Status
Personal Notes
Sign in to save personal notes for this topic.
Discussion
Sign in to join the discussion.