5.0 KiB
Dependencies
Use FastAPI dependencies for request-scoped wiring: sessions, auth, permission checks, and other collaborators that should be assembled by the framework before the handler runs.
Why this convention exists
Dependencies are FastAPI's main request-time composition tool. They let you:
- keep wiring visible in the handler signature
- reuse auth and resource access across routes
- give tests a clean override seam
- manage
yield-based resources with framework-controlled lifetime
That is better than opening sessions, parsing tokens, or repeating permission logic inside every route body.
The convention
- Put reusable request wiring in dependency callables.
- Use
Annotated[..., Depends(...)]aliases when they make signatures easier to read. - Keep handlers consuming already-shaped collaborators.
- Raise semantic HTTP errors inside dependencies when the failure is inherently about the request or caller.
- Use lifespan for app-wide startup resources; use dependencies for per-request access to them.
Follow it when the concern is request-scoped
Typical fits:
- DB/session access
- current-user lookup
- permission gates
- token/header parsing
- reusable request validation
Do not over-abstract it
Do not create a dependency just because a helper exists.
Skip dependency injection when:
- the logic is local to one route and trivial
- the helper is plain business logic with no FastAPI concerns
- the abstraction makes the signature harder to understand than the inline code would
Also avoid turning the dependency graph into a maze. If a reader has to chase six Depends(...) calls just to find the session, the abstraction is working against you.
Preferred shape
from typing import Annotated
from fastapi import Depends, HTTPException
def get_db() -> Generator[Session, None, None]:
with Session(engine) as session:
yield session
SessionDep = Annotated[Session, Depends(get_db)]
def get_current_user(session: SessionDep, token: TokenDep) -> User:
user = load_user_from_token(session, token)
if not user:
raise HTTPException(status_code=404, detail="User not found")
return user
CurrentUser = Annotated[User, Depends(get_current_user)]
@router.get("/items/{id}")
def read_item(session: SessionDep, current_user: CurrentUser, id: UUID) -> ItemPublic:
...
Why this works:
- the route tells you exactly what it needs
- auth and session logic are reusable
- tests can swap dependencies without patching route internals
Counterexamples
Hidden wiring inside handlers
@router.get("/items/{id}")
def read_item(id: UUID):
session = Session(engine)
token = parse_auth_header_somehow()
user = load_user(session, token)
...
Bad because resource and auth wiring are duplicated and easy to get inconsistently wrong.
Business logic buried in dependencies
def get_processed_report(...) -> Report:
# loads DB, runs domain rules, emails people, mutates state, etc.
Bad because dependencies should shape request collaborators, not become a second invisible service layer.
Startup work disguised as a dependency
If initialization is app-wide and long-lived, it belongs in lifespan, not in a request dependency that quietly performs setup.
Source signals
FastAPI
docs_src/bigger_applications/app_an_py310/dependencies.py:6-13uses a dedicated dependency callable for request validation and raises an HTTP error there.fastapi/routing.py:95-136wraps request handling inAsyncExitStack, which is the mechanism FastAPI uses to manage dependency lifetime, includingyield-based dependencies.tests/test_dependency_security_overrides.py:24-29defines a route whose collaborators come fromSecurity(...)andDepends(...)rather than inline setup.tests/test_dependency_security_overrides.py:44-63replaces those collaborators throughapp.dependency_overrides, which is the testing seam FastAPI expects you to use.
Full-stack FastAPI Template
backend/app/api/deps.py:21-27defines a yielded DB session dependency and exposes it asSessionDep.backend/app/api/deps.py:30-57decodes auth, loads the user, and raises semantic HTTP errors in a reusable current-user dependency.backend/app/api/routes/items.py:13-16shows the result in practice: the route signature receivesSessionDepandCurrentUserdirectly.
Starlette and FastAPI Users
starlette/applications.py:46-48says lifespan is the preferred replacement foron_startupandon_shutdown, reinforcing the split between app-wide resource setup and request-time dependencies.examples/beanie-oauth/app/app.py:17-28initializes Beanie in lifespan.examples/beanie-oauth/app/app.py:60-62protects a route withDepends(current_active_user)instead of doing auth lookup inline.
Bottom line
Dependencies should make request wiring more obvious, more reusable, and easier to test.
If a dependency hides core business behavior, it is probably the wrong abstraction.