Files
2026-06-01 21:42:05 +00:00

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

  1. Put reusable request wiring in dependency callables.
  2. Use Annotated[..., Depends(...)] aliases when they make signatures easier to read.
  3. Keep handlers consuming already-shaped collaborators.
  4. Raise semantic HTTP errors inside dependencies when the failure is inherently about the request or caller.
  5. 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-13 uses a dedicated dependency callable for request validation and raises an HTTP error there.
  • fastapi/routing.py:95-136 wraps request handling in AsyncExitStack, which is the mechanism FastAPI uses to manage dependency lifetime, including yield-based dependencies.
  • tests/test_dependency_security_overrides.py:24-29 defines a route whose collaborators come from Security(...) and Depends(...) rather than inline setup.
  • tests/test_dependency_security_overrides.py:44-63 replaces those collaborators through app.dependency_overrides, which is the testing seam FastAPI expects you to use.

Full-stack FastAPI Template

  • backend/app/api/deps.py:21-27 defines a yielded DB session dependency and exposes it as SessionDep.
  • backend/app/api/deps.py:30-57 decodes auth, loads the user, and raises semantic HTTP errors in a reusable current-user dependency.
  • backend/app/api/routes/items.py:13-16 shows the result in practice: the route signature receives SessionDep and CurrentUser directly.

Starlette and FastAPI Users

  • starlette/applications.py:46-48 says lifespan is the preferred replacement for on_startup and on_shutdown, reinforcing the split between app-wide resource setup and request-time dependencies.
  • examples/beanie-oauth/app/app.py:17-28 initializes Beanie in lifespan.
  • examples/beanie-oauth/app/app.py:60-62 protects a route with Depends(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.