---
title: "FastAPI: Dependency Injection"
slug: "fastapi-dependencies"
category: "backend"
tags: ["backend", "fastapi", "python", "dependency-injection", "depends", "testing"]
status: "stable"
last_updated: 2026-05-14
summary: "Depends() patterns for database sessions, auth, and shared state; nested deps, class-based deps, yield cleanup, and request scope."
related: ["[[backend/fastapi]]", "[[coding/python]]", "[[backend/fastapi-async-io]]", "[[backend/fastapi-pydantic]]", "[[backend/fastapi-lifespan]]", "[[backend/postgres]]", "[[coding/python-testing]]"]
---

## Overview

FastAPI's `Depends()` mechanism is the primary way to share resources across routes without threading state through function arguments. It handles database sessions, authentication, settings, and feature flags. The dependency graph is resolved per-request, cached within a request, and composable to arbitrary depth. This page expands on the `Depends` section in [[backend/fastapi]] with patterns for nested deps, class-based deps, and cleanup. See [[backend/fastapi-lifespan]] for application-scoped resources that live across requests.

## Keep route functions thin by pushing logic into dependencies

A route function should declare what it needs, then delegate. Business logic, access control, and resource acquisition belong in dependencies, not in the route body.

```python
from fastapi import APIRouter, Depends
from sqlalchemy.ext.asyncio import AsyncSession

router = APIRouter()

@router.get("/orders/{order_id}", response_model=OrderRead)
async def get_order(
    order_id: str,
    order: Order = Depends(get_order_or_404),
    user: User = Depends(get_current_user),
) -> Order:
    require_permission(user, "orders:read", order)
    return order
```

The route itself does one thing: check permission and return. Fetching the order and resolving the user are separate dependency functions tested independently.

## Chain dependencies to express context hierarchy

A dependency can itself declare `Depends()`. FastAPI builds a directed acyclic graph and resolves each node once per request.

```python
async def get_db() -> AsyncGenerator[AsyncSession, None]:
    async with async_session_factory() as session:
        yield session

async def get_current_user(
    token: str = Depends(oauth2_scheme),
    db: AsyncSession = Depends(get_db),
) -> User:
    return await auth_service.resolve(db, token)

async def get_admin_user(user: User = Depends(get_current_user)) -> User:
    if not user.is_admin:
        raise HTTPException(status_code=403)
    return user
```

`get_db` runs once per request even if both `get_current_user` and the route body declare it. The session object is cached at the request scope. See [[backend/fastapi-async-io]] for why the session must be async.

## Use `yield` for resource cleanup

A dependency that opens a resource should yield it, then close it. FastAPI calls the code after `yield` after the response is sent.

```python
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker

SessionLocal: async_sessionmaker[AsyncSession] = ...  # set in lifespan

async def get_db() -> AsyncGenerator[AsyncSession, None]:
    async with SessionLocal() as session:
        try:
            yield session
            await session.commit()
        except Exception:
            await session.rollback()
            raise
```

Wrap the yield in a `try/except` to roll back on errors. FastAPI catches exceptions from dependencies and re-raises them after running cleanup. Never hold a database connection across `yield` longer than one request; see [[backend/postgres]] for connection pool sizing.

## Prefer class-based dependencies for configurable behavior

When a dependency needs configuration, a class with `__call__` is cleaner than a factory of closures.

```python
class RateLimiter:
    def __init__(self, max_calls: int, window_seconds: int) -> None:
        self.max_calls = max_calls
        self.window_seconds = window_seconds

    async def __call__(self, request: Request) -> None:
        key = f"rl:{request.client.host}"
        if await exceeds_limit(key, self.max_calls, self.window_seconds):
            raise HTTPException(status_code=429)

strict_limit = RateLimiter(max_calls=10, window_seconds=60)

@router.post("/payments", dependencies=[Depends(strict_limit)])
async def create_payment(...) -> PaymentRead: ...
```

Class instances are reused across requests. Stateful dependencies (counters, caches) stored on the instance will be shared; use external storage or `app.state` for anything that must persist. See [[backend/fastapi-lifespan]] for initializing shared state.

## Use `Annotated` to DRY repeated dependency declarations

`Annotated` from `typing` lets you name a type-plus-dependency pair once and reuse it.

```python
from typing import Annotated
from fastapi import Depends

CurrentUser = Annotated[User, Depends(get_current_user)]
DBSession = Annotated[AsyncSession, Depends(get_db)]

@router.get("/profile")
async def profile(user: CurrentUser, db: DBSession) -> UserRead:
    return await user_service.get(db, user.id)
```

This removes repeated `= Depends(...)` boilerplate across dozens of routes and makes the dependency visible in the type signature. See [[backend/fastapi-pydantic]] for how these typed values integrate with response models.

## Override dependencies in tests without monkey-patching

FastAPI provides `app.dependency_overrides` to swap a dependency in tests without patching global state.

```python
from fastapi.testclient import TestClient

def fake_db():
    yield test_session

app.dependency_overrides[get_db] = fake_db
client = TestClient(app)
```

Reset `app.dependency_overrides = {}` in teardown to avoid state leaking across tests. See [[coding/python-testing]] for the full fixture and isolation strategy.

## Related

- [[backend/fastapi]]
- [[coding/python]]
- [[backend/fastapi-async-io]]
- [[backend/fastapi-pydantic]]
- [[backend/fastapi-lifespan]]
- [[backend/postgres]]
- [[coding/python-testing]]
