FastAPI dependency injection for agentic API auth
Every route handler re-parses the JWT, re-hits the database, and you can't test any of it
You shipped JWT auth for your agent API. Every route handler starts with the same 15 lines: read the Authorization header, parse the token, query the user table, check tenancy, attach the user to a local variable. After 8 routes, you have 120 lines of copy-paste auth. The first time you test one, you have to mock the request header manually and instantiate the database. You give up testing auth entirely.
The fix is FastAPI's dependency injection. Write the auth function once, declare it as a Depends parameter in every route, and FastAPI wires it up for you. Tests override the dependency with a fake. The auth code lives in one place and runs on every request with zero duplication.
This post is the FastAPI Depends pattern for agent auth: the single-function auth dependency, the sub-dependency chain, how tenancy resolution flows in, and the override pattern that makes tests instant.
Why is duplicated auth code dangerous?
3 specific failure modes:
- Drift. Route A checks tenancy. Route B forgets. A month later someone finds a route that returns data from the wrong tenant.
- Slow tests. Each route test has to mock the
Authorizationheader and the database query. Tests become integration tests. - Inconsistent error messages. Route A returns 401 for expired tokens. Route B returns 403 because someone had a different idea. Clients see inconsistent errors.
FastAPI Depends fixes all 3 by lifting the auth function out of the route handlers and making it a reusable callable.
graph LR
Request[HTTP request] --> FastAPI[FastAPI]
FastAPI --> Auth[get_auth dependency]
Auth --> Token[extract + verify token]
Auth --> User[load user from db]
Auth --> Tenant[resolve tenant]
Auth --> Context[AuthContext]
Context --> Route[Route handler]
style Auth fill:#dcfce7,stroke:#15803d
style Context fill:#dbeafe,stroke:#1e40af
What does the auth dependency look like?
One function that returns an AuthContext object. Every route takes auth: AuthContext = Depends(get_auth) and uses auth.user_id and auth.tenant_id without re-parsing anything.
# filename: app/auth.py
# description: Single auth dependency used by every protected route.
from dataclasses import dataclass
from fastapi import Header, HTTPException, Depends
from sqlmodel import Session as DbSession
from app.infra.db import get_db
from app.infra.tokens import verify_access_token
from app.infra.models import User
@dataclass
class AuthContext:
user_id: str
tenant_id: int
role: str
async def get_auth(
authorization: str | None = Header(None),
db: DbSession = Depends(get_db),
) -> AuthContext:
if not authorization or not authorization.startswith('Bearer '):
raise HTTPException(401, 'missing bearer token')
token = authorization[7:]
try:
claims = verify_access_token(token)
except ValueError as exc:
raise HTTPException(401, f'invalid token: {exc}')
user = db.get(User, claims['sub'])
if not user:
raise HTTPException(401, 'unknown user')
return AuthContext(
user_id=str(user.id),
tenant_id=user.tenant_id,
role=user.role,
)
3 things are doing real work. get_auth takes db as a sub-dependency, so FastAPI wires in a database session automatically. The AuthContext dataclass carries everything a route handler needs for tenancy and RBAC. HTTPException gives a clean 401 that every route inherits.
Every protected route now reads:
@router.post('/chat')
async def chat(body: ChatRequest, auth: AuthContext = Depends(get_auth)):
result = await run_chat_turn(auth.user_id, auth.tenant_id, body.message)
return result
15 lines of auth boilerplate per route → 1 line.
How do sub-dependencies chain together?
FastAPI walks the dependency graph. If get_auth depends on get_db, and the route depends on get_auth, FastAPI resolves get_db once per request, passes it to get_auth, passes the result to the route. Same get_db instance is reused for the route body if the route also depends on it.
This is the killer feature for agent services: you can build a tall dependency chain (tenant → user → session → context) and every route just asks for the top-level dependency. FastAPI resolves everything below.
# filename: app/deps.py
# description: Chained dependencies for tenant + session resolution.
async def get_current_tenant(auth: AuthContext = Depends(get_auth)) -> int:
return auth.tenant_id
async def get_session(
session_id: str,
tenant: int = Depends(get_current_tenant),
db: DbSession = Depends(get_db),
):
from app.infra.models import Session as AgentSession
s = db.get(AgentSession, session_id)
if not s or s.tenant_id != tenant:
raise HTTPException(404, 'session not found')
return s
Any route that needs a session just takes session: AgentSession = Depends(get_session). Tenant check is automatic. Unknown or cross-tenant sessions 404 before the route body runs.
For the broader multi-tenant schema this pattern sits on, see the User and session models for multi-tenant AI agents post.
How do you test a route with dependency overrides?
FastAPI lets you override any dependency with a test-specific one. Inject a fake AuthContext without mocking headers or database calls.
# filename: tests/test_chat_route.py
# description: Route test with auth dependency overridden.
from fastapi.testclient import TestClient
from app.main import app
from app.auth import get_auth, AuthContext
def fake_auth() -> AuthContext:
return AuthContext(user_id='test-user', tenant_id=1, role='member')
app.dependency_overrides[get_auth] = fake_auth
client = TestClient(app)
def test_chat_happy_path():
r = client.post('/chat', json={'message': 'hello', 'session_id': 'abc'})
assert r.status_code == 200
No header mocking, no database fixture, no JWT generation. Every route is testable with 3 lines of setup.
For the JWT auth internals that verify_access_token implements, see the JWT authentication for agentic APIs post.
Why should you avoid module-level globals for dependencies?
Because module globals break in 3 ways at scale:
- Shared mutable state across workers. If you instantiate a database engine at module import time, every worker gets its own copy but they all share file descriptors in weird ways under fork.
- Test pollution. A global
dbin moduleapp.infra.dbmakes it impossible to swap the database in tests without monkey-patching. - Hidden dependencies. A function that reads from a global
dbhas an invisible dependency. You cannot see it in the function signature, so you cannot reason about what the function needs to run.
Depends makes every dependency explicit at the function signature level. The function declares what it needs; FastAPI provides it; tests override it. No hidden state.
For the full production FastAPI stack this all sits inside, see the FastAPI and Uvicorn for production agentic AI systems post.
What to do Monday morning
- Find the auth code in your route handlers. If there are 3 or more copies of the same 10 lines, extract into a
get_authdependency. - Create an
AuthContextdataclass with whatever fields your routes actually read (user_id, tenant_id, role, plan). Not more. - Replace every route's manual header parsing with
auth: AuthContext = Depends(get_auth). Delete the duplicated code. - Add
app.dependency_overrides[get_auth] = fake_authin your test fixtures. Watch your route tests drop to under 100 ms each. - Chain any sub-dependencies (tenant resolver, session loader, role checker) on top of
get_auth. Each becomes a one-liner for route handlers.
The headline: FastAPI Depends turns 15 lines of auth boilerplate per route into 1 line, makes every route testable with a 3-line fake, and eliminates module-level globals. 30 minutes to migrate a whole service.
Frequently asked questions
What is FastAPI's Depends system?
Depends is FastAPI's dependency injection mechanism. You declare a function as a Depends parameter in a route handler, and FastAPI calls that function before the route body runs, passing the result into the handler. Dependencies can chain: one dependency can depend on another. This lets you build composable auth, database sessions, and context resolution without module-level globals.
Why is Depends better than module-level globals for database sessions?
Because module-level globals are invisible in function signatures and break under fork for multi-worker deployments. Depends makes every dependency explicit, lets FastAPI reuse instances within a request, and supports test overrides. A route that reads a global has hidden state; a route with Depends(get_db) declares exactly what it needs.
How do you test a FastAPI route that requires authentication?
Use app.dependency_overrides to swap the real get_auth function with a fake that returns a canned AuthContext. The test client then sends requests without needing to generate a real JWT or mock the Authorization header. This is the canonical pattern and runs tests in under 100 ms each.
Can dependencies chain into other dependencies?
Yes. FastAPI walks the dependency graph and resolves each node once per request. You can build tall chains: get_auth → get_db → get_current_user → get_current_tenant → get_session. Each level only declares what it needs; FastAPI wires the rest. Same resolved instance is reused anywhere in the route body that asks for it.
When should you use sub-dependencies vs inline logic in the route handler?
Use a sub-dependency when the same logic is needed in 2 or more routes, or when the logic can fail with a 401/403/404 that should short-circuit the route body. Inline is fine for logic specific to one route. The rule: if the code could be a reusable auth check, tenancy check, or resource loader, it belongs in a dependency.
Key takeaways
- Duplicated auth in every route handler drifts, slows tests, and makes error messages inconsistent. Extract into a single
get_authdependency. - FastAPI's
Dependsresolves the dependency graph automatically. Declare once, use everywhere, override in tests with 3 lines. - Sub-dependencies chain.
get_auth → get_current_tenant → get_sessiongives you a clean top-level API where each route takes only what it needs. - Use
app.dependency_overrides[get_auth] = fake_authin tests. No header mocking, no database fixtures, no JWT generation. - Avoid module-level globals. Every dependency belongs in a function signature, not a global variable.
- To see
Depends-based auth wired into a full production agent stack with JWT rotation and multi-tenancy, walk through the Build your own coding agent course, or start with the AI Agents Fundamentals primer.
For the full FastAPI dependency injection documentation with nested dependencies, class-based dependencies, and advanced patterns, see the FastAPI dependencies guide. The override pattern for testing is documented under "Testing Dependencies with Overrides".
Continue Reading
Ready to go deeper?
Go beyond articles. Build production AI systems with hands-on workshops and our intensive AI Bootcamp.