Dependency Injection

Published by Programming Line in FastAPI. Last updated .

FastAPI dependency injection is the mechanism that lets a path operation declare the collaborators it needs instead of constructing them inside the endpoint body. In this lesson, the outcome is concrete: you will be able to compose request parsing, authentication, database sessions, pagination, and service objects through FastAPI’s dependency graph, then test and troubleshoot that graph deliberately.

Within this course section on dependencies and configuration, dependency injection is the bridge between simple endpoints and maintainable application structure. Configuration values, clients, sessions, and policy checks can be supplied at the boundary of a request while the endpoint stays focused on the use case it serves.

Purpose and Request Outcome

A FastAPI dependency is any callable that FastAPI can call while handling a request. It can be a function, class, callable instance, generator function, or async variant. You attach it with Depends, usually inside typing.Annotated. FastAPI inspects the callable’s signature, resolves its own parameters, validates request data for those parameters, calls dependencies in the required order, and injects each return value into the next callable that asked for it.

The endpoint receives ready-to-use values: a decoded user, a checked feature flag, a bounded page size, a database session, or a domain service. When a dependency raises HTTPException, request processing stops and the response is built from that exception. When a dependency uses yield, FastAPI runs its cleanup code after the response path has finished, which makes it suitable for scoped resources such as sessions.

How FastAPI Builds the Dependency Graph

FastAPI starts with the path operation function and recursively walks every parameter declared with Depends. Each dependency may have ordinary request parameters such as Query, Header, or Cookie, and it may also depend on other dependencies. The result is a directed graph for the route. At runtime, FastAPI solves that graph for the current request.

By default, each dependency result is cached within one request. If two downstream dependencies ask for the same dependency with the same declaration, FastAPI calls it once and reuses the value. This matters for authentication and session creation: one token decode or one session provider can feed several services without repeated work. You can opt out with Depends(provider, use_cache=False) when the dependency must run every time it is requested.

Resolution order follows the graph, not the visual order in the endpoint alone. Sub-dependencies run before the dependency that needs their values. Validation errors in dependency parameters produce the same 422-style response shape as validation errors in endpoint parameters. Exceptions raised inside dependencies behave like exceptions in endpoints. Cleanup for yield dependencies is stacked so resources are released reliably after the dependent code has run.

API Anatomy

The modern shape is Annotated[T, Depends(callable)]. T is the type the endpoint expects to receive. Depends(callable) tells FastAPI which provider to execute. If the callable is omitted, FastAPI can sometimes infer it from the annotated type, but explicit providers are clearer in application code.

Dependencies can appear in three places. A parameter dependency injects a value into a function argument. A decorator-level dependency, passed with dependencies=[Depends(check)], runs for its side effects and does not pass a value to the endpoint. Router-level and application-level dependencies apply a policy or setup step across groups of routes. Choose the narrowest scope that expresses the behavior: parameter for data the endpoint uses, route for one policy, router for a family of routes, and app-level only for truly global requirements.

Example 1: Shared Pagination

from typing import Annotated

from fastapi import Depends, FastAPI, Query

app = FastAPI()


def pagination(
    limit: Annotated[int, Query(ge=1, le=100)] = 20,
    offset: Annotated[int, Query(ge=0)] = 0,
) -> dict[str, int]:
    return {"limit": limit, "offset": offset}


@app.get("/items")
def list_items(page: Annotated[dict[str, int], Depends(pagination)]):
    return {"limit": page["limit"], "offset": page["offset"]}

The pagination dependency owns the query parameter rules. A request to GET /items?limit=5&offset=10 returns {"limit": 5, "offset": 10}. A request with limit=500 fails validation before list_items runs because the dependency’s Query(le=100) constraint is part of the route graph. This is the smallest useful dependency: it removes duplicate parameter declarations without hiding business logic.

Example 2: Authentication Feeding a Service

from typing import Annotated

from fastapi import Depends, FastAPI, Header, HTTPException, status

app = FastAPI()

TOKENS = {"dev-token": {"username": "ada", "role": "admin"}}


def current_user(authorization: Annotated[str | None, Header()] = None) -> dict[str, str]:
    if authorization is None or not authorization.startswith("Bearer "):
        raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Missing bearer token")
    token = authorization.removeprefix("Bearer ")
    user = TOKENS.get(token)
    if user is None:
        raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid token")
    return user


class ProjectService:
    def __init__(self, user: dict[str, str]):
        self.user = user

    def visible_projects(self) -> list[dict[str, str]]:
        return [{"name": "dependency-map", "owner": self.user["username"]}]


def project_service(user: Annotated[dict[str, str], Depends(current_user)]) -> ProjectService:
    return ProjectService(user)


@app.get("/projects")
def list_projects(service: Annotated[ProjectService, Depends(project_service)]):
    return service.visible_projects()

Here project_service depends on current_user, and the endpoint depends on project_service. A request with header Authorization: Bearer dev-token returns [{"name": "dependency-map", "owner": "ada"}]. A missing or unknown token returns 401 and the service is never constructed. This structure keeps protocol parsing in one layer, identity policy in another, and project behavior in the service.

Example 3: Request-Scoped Session Cleanup

from collections.abc import Generator
from typing import Annotated

from fastapi import Depends, FastAPI

app = FastAPI()


class FakeSession:
    def __init__(self) -> None:
        self.closed = False

    def execute(self, statement: str) -> list[str]:
        return [f"ran: {statement}"]

    def close(self) -> None:
        self.closed = True


def get_session() -> Generator[FakeSession, None, None]:
    session = FakeSession()
    try:
        yield session
    finally:
        session.close()


@app.get("/reports")
def reports(session: Annotated[FakeSession, Depends(get_session)]):
    rows = session.execute("select name from reports")
    return {"rows": rows}

The get_session dependency is a generator. Code before yield acquires the resource, the yielded object is injected into the endpoint, and the finally block runs when request handling leaves that scope. In a real application the fake session would be a SQLAlchemy or SQLModel session. The behavior is important either way: the endpoint does not own cleanup, so the same session lifetime can be reused consistently across routes.

Design Choices and Trade-offs

Dependency injection is most valuable when it makes lifetimes and policies explicit. Put request-derived facts such as current user, tenant, locale, and page bounds in dependencies. Put use-case behavior in services that can be tested without ASGI. Put storage details behind session or repository providers. Avoid turning every one-line helper into a dependency; too many tiny providers make the graph harder to read than ordinary Python calls.

Function dependencies are simple and transparent. Class dependencies work well when constructor parameters describe configuration or when a callable object carries state. Generator dependencies are appropriate for resources with open and close phases. Global singletons may be acceptable for immutable clients, but request-specific state should not live in module globals because concurrent requests can interleave.

Decorator-level dependencies are good for authorization checks, rate-limit checks, or audit hooks where the endpoint does not need the returned value. The trade-off is discoverability: a reader must inspect the route decorator or router to see why a request can fail. For values used by the endpoint, prefer parameter dependencies because the function signature documents the contract.

Failure Modes and Troubleshooting

Symptom: the endpoint returns 422 before your function breakpoint is hit. Cause: a dependency parameter failed validation, often a missing header or an out-of-range query value. Diagnose: inspect the response body location; it will point to query, header, cookie, or another input location. Correction: adjust the request, make the parameter optional, or provide a default that matches the intended contract.

Symptom: a dependency is called only once even though several services request it. Cause: per-request dependency caching is working. Diagnose: add a temporary counter or log statement to the provider and call a route that uses it twice through sub-dependencies. Correction: keep the default for expensive or identity-producing dependencies; use use_cache=False only for providers that intentionally produce a fresh value on every injection.

Symptom: tests keep using the production database or authentication provider. Cause: the test did not override the exact callable object used in Depends. Diagnose: check app.dependency_overrides and compare keys with the provider functions imported by the route module. Correction: set app.dependency_overrides[get_session] = fake_session_provider or override the auth dependency, then clear overrides after the test.

Symptom: connections accumulate under load. Cause: a resource dependency returns a session without closing it, or cleanup code is unreachable. Diagnose: inspect pool metrics and confirm the provider uses yield with cleanup in finally. Correction: move acquisition and release into a generator dependency and keep transaction ownership explicit.

Security, Performance, and Reliability

Authentication dependencies should fail closed. Missing credentials, malformed tokens, inactive accounts, and insufficient scopes should stop request processing before domain services run. Avoid logging raw tokens or secrets from headers. When dependencies construct clients or sessions from settings, keep secrets in configuration providers rather than endpoint code.

Performance depends on dependency cost and scope. Cheap validation dependencies are fine on every route. Expensive network calls should be cached within the request or moved behind a service that uses timeouts. Do not perform slow startup work in a request dependency if the result is immutable; initialize that resource during application lifespan and inject a lightweight handle.

Reliability improves when cleanup is centralized. A generator dependency can close sessions even when the endpoint raises an exception. Tests using dependency overrides can simulate outages, expired credentials, and empty repositories without changing route code, which makes failure behavior easier to verify.

Hands-on Lab: Build and Verify a Dependency Graph

Prerequisites: Python with FastAPI installed, plus pytest if you want to run the test function directly. Create a file named main.py with the following code.

from typing import Annotated

from fastapi import Depends, FastAPI, Header, HTTPException, Query
from fastapi.testclient import TestClient

app = FastAPI()

API_KEYS = {"course-key": "instructor"}


def api_key(x_api_key: Annotated[str | None, Header()] = None) -> str:
    if x_api_key not in API_KEYS:
        raise HTTPException(status_code=401, detail="Invalid API key")
    return API_KEYS[x_api_key]


def paging(limit: Annotated[int, Query(ge=1, le=50)] = 10) -> int:
    return limit


@app.get("/lessons")
def lessons(owner: Annotated[str, Depends(api_key)], limit: Annotated[int, Depends(paging)]):
    return {"owner": owner, "limit": limit, "items": ["dependencies", "settings"][:limit]}


def test_lessons_dependency_graph() -> None:
    client = TestClient(app)
    ok = client.get("/lessons?limit=1", headers={"x-api-key": "course-key"})
    assert ok.status_code == 200
    assert ok.json() == {"owner": "instructor", "limit": 1, "items": ["dependencies"]}

    missing = client.get("/lessons")
    assert missing.status_code == 401

    invalid_limit = client.get("/lessons?limit=100", headers={"x-api-key": "course-key"})
    assert invalid_limit.status_code == 422
  1. Run the app with uvicorn main:app --reload.
  2. Call GET /lessons?limit=1 with header x-api-key: course-key.
  3. Verify the JSON response is {"owner": "instructor", "limit": 1, "items": ["dependencies"]}.
  4. Call the same route without the header and verify the response status is 401.
  5. Call GET /lessons?limit=100 with the valid header and verify the response status is 422 because the dependency constrains limit to at most 50.
  6. Run pytest main.py to execute the three checks in test_lessons_dependency_graph.

Cleanup is simple for this lab: stop Uvicorn and delete the temporary main.py file. In a real service, also clear any test dependency overrides after each test so one test does not change the dependency graph seen by the next test.

Assessment Exercises

  • Refactor an endpoint that opens a database session inside the function body into a yield dependency. Explain what changes about cleanup and testability.
  • Design dependencies for a route that needs a current user, tenant settings, and a project service. Which dependency should depend on which, and why?
  • A route returns 422 when a required header is missing from a sub-dependency. Describe how you would locate the failing dependency from the response body and code.
  • Write a test that overrides authentication to return a fixed user, then explain why clearing app.dependency_overrides matters.
  • When would you choose a router-level dependency instead of a parameter dependency? Give one case where that choice would be worse.

Summary

FastAPI dependency injection turns a route into a solved graph of providers. The framework inspects callable signatures, validates dependency inputs, executes sub-dependencies first, caches values within a request, injects return values, and runs generator cleanup for scoped resources. Use dependencies for request facts, policies, configuration handles, sessions, and service construction. Keep business decisions testable outside the framework, override dependencies in tests, and troubleshoot by following the graph from endpoint parameters back through each provider.