Skip to content
C Codeloom
Backend

FastAPI Dependency Injection Explained

How FastAPI's Depends system actually works, the lifecycle of dependencies, scoping with yield, and patterns for testable, layered FastAPI apps.

·5 min read · By Codeloom
Intermediate 10 min read

What you'll learn

  • How Depends resolves and caches per request
  • Yield dependencies and resource lifecycle
  • Layering: settings -> session -> repository -> service
  • Dependency overrides for tests
  • Common DI pitfalls in FastAPI

Prerequisites

  • Basic Python
  • Some exposure to FastAPI

What and why

FastAPI’s dependency injection is the most underused part of the framework. Most tutorials show it as a way to read query parameters, then move on. In real apps, Depends is how you compose your application: configuration, database sessions, authentication, repositories, and feature flags all flow through it.

The win is testability. Anything injected via Depends can be swapped at test time without monkeypatching. The cost is learning how the resolver behaves around scopes and yields.

Mental model

For every incoming request, FastAPI walks the route handler’s signature, finds every Depends(...), and builds a dependency graph. Each unique dependency is resolved once per request and cached. Generators (yield) double as setup/teardown: code before yield runs at injection, code after runs after the response is sent.

Request --> route handler(user, db, settings)
               |
               v
        build dependency graph
               |
 +-------------+-------------+
 |             |             |
get_settings  get_db        get_user
 (cached)     |             |
              v             v
       create Session   verify token
              |             |
              v             v
      yield to handler   yield User
              |
          handler runs
              |
              v
     teardown in reverse:
       session.close()
       release lock
Per-request dependency graph

Hands-on example

A typical four-layer setup: settings, session, repository, service.

from functools import lru_cache
from fastapi import Depends, FastAPI, HTTPException
from sqlalchemy.orm import Session, sessionmaker
from sqlalchemy import create_engine
from pydantic_settings import BaseSettings

class Settings(BaseSettings):
    database_url: str

@lru_cache
def get_settings() -> Settings:
    return Settings()

def get_engine(settings: Settings = Depends(get_settings)):
    return create_engine(settings.database_url)

def get_db(engine = Depends(get_engine)):
    SessionLocal = sessionmaker(bind=engine)
    db = SessionLocal()
    try:
        yield db
    finally:
        db.close()

class UserRepo:
    def __init__(self, db: Session): self.db = db
    def get(self, uid: str): ...

def get_user_repo(db: Session = Depends(get_db)) -> UserRepo:
    return UserRepo(db)

class UserService:
    def __init__(self, repo: UserRepo): self.repo = repo
    def fetch(self, uid: str):
        user = self.repo.get(uid)
        if not user:
            raise HTTPException(404, "user not found")
        return user

def get_user_service(repo: UserRepo = Depends(get_user_repo)) -> UserService:
    return UserService(repo)

app = FastAPI()

@app.get("/users/{uid}")
def read_user(uid: str, svc: UserService = Depends(get_user_service)):
    return svc.fetch(uid)

Notice the layering. The route handler does not know about the database; it only knows about the service. The service does not know about SQLAlchemy; it only knows about the repository. Each layer is testable in isolation.

For testing, override any dependency:

from fastapi.testclient import TestClient

def fake_user_service():
    svc = UserService(repo=FakeRepo())
    return svc

app.dependency_overrides[get_user_service] = fake_user_service
client = TestClient(app)

dependency_overrides is a dict you mutate before tests run. The framework checks it on every resolution, so swaps are immediate.

Yield dependencies are how you manage anything with setup and teardown: sessions, locks, files, distributed traces. Code after yield runs even if the handler raised, which makes it the right place for cleanup.

def get_lock(name: str = "default"):
    lock = redis.lock(name, blocking_timeout=5)
    acquired = lock.acquire()
    try:
        yield lock
    finally:
        if acquired:
            lock.release()

Common pitfalls

Forgetting that dependencies are cached per request. If you call get_db from two routes via include_router, you get one session, not two. That is usually what you want; surprises happen when you assume otherwise.

use_cache=False disables caching for a single dependency. Useful for nonces or audit timestamps that should be re-evaluated.

Async vs sync mismatch. FastAPI happily mixes them, but a sync dependency that blocks (e.g., requests.get) will stall the event loop. Use async clients or push blocking calls to a thread pool with run_in_threadpool.

Heavy work inside dependencies runs on every request. A dependency that opens a connection pool is fine; one that builds the pool on every call is a disaster. Make pool builders module-level singletons or wrap them with lru_cache.

Circular dependencies happen when two helpers depend on each other. The resolver will raise; refactor to a common upstream dependency.

Production tips

Type your dependencies. The return type drives autocomplete in handlers and lets type checkers verify the wiring. Use Annotated[Session, Depends(get_db)] for cleaner signatures in modern code.

from typing import Annotated
SessionDep = Annotated[Session, Depends(get_db)]

@app.get("/users/{uid}")
def read_user(uid: str, db: SessionDep): ...

Keep dependencies pure when possible. A function that just reads config and constructs an object is easy to test. One that performs network calls during construction is not.

Push authentication into a dependency. current_user: User = Depends(get_current_user) makes every protected route obvious by inspection.

Use Depends for feature flags too. Inject a FeatureSet and let routes ask if flags.beta_search. This is easier to reason about than scattered environment lookups.

Wrap-up

Depends is the wiring layer of a FastAPI app. Use it to compose settings, sessions, repositories, and services, with yield dependencies for setup/teardown. Override in tests instead of monkeypatching. Type everything, cache singletons at module scope, and respect the sync/async boundary. Once the layering is in place, the routes become two-line orchestrators and the rest of your code is plain Python.