Home/Learn/FastAPI/DI Patterns — Auth Chains, Router Guards & Test Overrides

DI Patterns — Auth Chains, Router Guards & Test Overrides

Advanced
Dependency Injection

Real 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.

token → user → role: each endpoint declares its guard level
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.

Guard the router, not each route; override nodes in tests
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 Aria
1

Design role-based access for student/TPO/admin in FastAPI — show the dependency chain.

HardFlipkart
2

How do you guarantee no endpoint in /admin ships unprotected?

MediumPaytm
3

How does dependency_overrides work, and why is it better than monkeypatching auth in tests?

HardPostman

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.

Loading discussion…