Dependency Injection Strategies in FastAPI

Dependency injection in FastAPI is the mechanism that resolves what a route handler needs — a database session, the current user, a configured client — from the function signature, per request, with the entire graph visible to the type checker.

This is the pattern that keeps HTTP concerns out of your domain logic, and it underpins everything else in Core Architecture and Routing Patterns. The application factory assembles the providers, configuration is itself delivered as a dependency, and testing relies on overriding the graph. Get injection right and the rest of the architecture composes cleanly.

A FastAPI dependency resolution graph for one request A route handler depends on a current-user dependency and a database session dependency. The current-user dependency depends on token decoding, which depends on settings. The settings node is shared and resolved once per request. Route handler GET /orders/{id} current_user Depends db_session yield · cleanup decode_token get_settings cached per request
A request builds a directed graph of providers. Shared nodes such as settings are resolved once and reused, and yield dependencies clean up after the response.

Core Mechanics: Resolution, Caching, and Scope

When a request arrives, FastAPI walks the handler's parameters, resolves each Depends(...) provider, and recurses into sub-dependencies until the graph is satisfied. Within a single request, a provider is resolved once and its result cached, so two dependencies that both need settings share one resolution. This per-request memoization is what makes fine-grained dependencies cheap.

from typing import Annotated

from fastapi import Depends

from app.config import Settings, get_settings


async def decode_token(
    token: str,
    settings: Annotated[Settings, Depends(get_settings)],
) -> dict[str, str]:
    # get_settings is resolved once per request and reused by every consumer.
    return verify_jwt(token, settings.secret_key.get_secret_value())

Production Implementation: Yield Dependencies and Cleanup

The most important production pattern is the yield dependency, which behaves like a context manager: setup before yield, teardown after the response. This is the correct lifecycle for a database session, ensuring it is committed or rolled back and always closed, even on error.

from collections.abc import AsyncGenerator
from typing import Annotated

from fastapi import Depends, Request
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker


async def get_db_session(request: Request) -> AsyncGenerator[AsyncSession, None]:
    factory: async_sessionmaker[AsyncSession] = request.app.state.session_factory
    async with factory() as session:   # Acquired per request from the pooled engine.
        try:
            yield session
            await session.commit()
        except Exception:
            await session.rollback()   # Teardown runs even when the handler raised.
            raise


SessionDep = Annotated[AsyncSession, Depends(get_db_session)]

Aliasing the dependency as SessionDep keeps handler signatures readable and gives you one place to change the wiring.

Router- and App-Level Dependencies

Cross-cutting gates such as authentication belong on the router, not in every handler. Dependencies attached to an APIRouter run for every route in it:

from fastapi import APIRouter, Depends

# verify_api_key runs for every route in this router without cluttering signatures.
admin_router = APIRouter(prefix="/admin", dependencies=[Depends(verify_api_key)])

Async and Performance Notes

A dependency declared async def runs on the event loop; a plain def dependency is executed in a thread pool so it cannot block the loop. Keep provider work light — a dependency that performs an unbounded query on every request multiplies that cost across your whole API. Where a dependency is genuinely expensive and request-independent, cache it at application scope rather than recomputing per request.

Testing Strategy

The declarative graph is what makes testing tractable: replace any node with app.dependency_overrides.

def test_orders_requires_auth(client, app):
    app.dependency_overrides[get_db_session] = fake_session
    app.dependency_overrides[decode_token] = lambda: {"sub": "test-user"}
    # ... assert the route behaves with the injected doubles.

The detailed best-practice walk-through, including circular-import avoidance, is in Best Practices for FastAPI Dependency Injection.

Failure Modes and Debugging

  • Circular imports that stall startup. Wire dependencies in the factory or import inside functions; never have routers and services import each other at module scope.
  • Sessions that never close. A non-yield dependency that opens a session leaks connections; use the yield form so teardown always runs.
  • Accidental caching surprises. If you need a fresh value within one request, pass use_cache=False.
  • Blocking work in async providers. Move blocking calls off the loop, a topic developed in Async Correctness and Concurrency.