Dependency Injection — How Depends() Works

FastAPI’s dependency injection system is its most powerful architectural feature — it makes shared logic (database sessions, authentication, settings, rate limiting) reusable across route handlers with zero code duplication. When you declare a parameter as Depends(some_function), FastAPI calls some_function, resolves its own parameters (which may themselves be dependencies), and passes the result to your handler. The system handles lifecycle (setup and teardown), caching (calling the same dependency only once per request), and testing (replacing real dependencies with fakes via app.dependency_overrides).

How Depends() Works

from fastapi import FastAPI, Depends

app = FastAPI()

# ── A simple dependency — a regular function ──────────────────────────────────
def get_greeting(name: str = "World") -> str:
    return f"Hello, {name}!"

@app.get("/greet")
def greet(message: str = Depends(get_greeting)):
    return {"message": message}
# GET /greet           → {"message": "Hello, World!"}
# GET /greet?name=Alice → {"message": "Hello, Alice!"}

# FastAPI resolves get_greeting's parameters from the request,
# calls get_greeting(name="Alice"), and passes the result to greet()

# ── Dependency resolving its own dependencies ─────────────────────────────────
def get_base_url(request: Request) -> str:
    return str(request.base_url)

def get_config(base_url: str = Depends(get_base_url)) -> dict:
    return {"base_url": base_url, "version": "1.0.0"}

@app.get("/info")
def get_info(config: dict = Depends(get_config)):
    return config   # base_url automatically extracted from request
Note: FastAPI caches dependency results within a single request by default — if two route handlers in the same request chain both depend on get_db, FastAPI calls get_db only once and passes the same session to both. This “request-scoped singleton” behaviour is correct for database sessions (one session per request) but may be wrong for other dependencies. To disable caching for a specific dependency use Depends(get_something, use_cache=False) — FastAPI will call it fresh each time it appears.
Tip: Dependencies can be any callable — functions, async functions, classes with __call__, or class constructors. Class-based dependencies are useful for grouping related parameters and keeping state between dependency methods. The class is instantiated per-request: class Pagination: def __init__(self, page: int = 1, size: int = 10): ... — FastAPI instantiates Pagination(page=2, size=20) from the request query params and injects the instance.
Warning: Dependency functions that use yield (generator dependencies) have setup code before the yield and teardown code after. The teardown code runs after the route handler returns its response — not after the response is sent to the client. This is important for database sessions: db.close() in the finally block of a get_db dependency runs after your handler returns but before the HTTP response is fully sent. Never assume the teardown has run when the client receives the response.

Sub-Dependencies

from fastapi import FastAPI, Depends, Header, HTTPException

app = FastAPI()

# ── Dependency chain: A → B → C ───────────────────────────────────────────────
def get_token(authorization: str | None = Header(default=None)) -> str:
    if not authorization or not authorization.startswith("Bearer "):
        raise HTTPException(401, "Missing or invalid Authorization header")
    return authorization.removeprefix("Bearer ")

def get_user_id_from_token(token: str = Depends(get_token)) -> int:
    # In reality: decode JWT, validate signature, extract sub claim
    if token == "test-token-user-5":
        return 5
    raise HTTPException(401, "Invalid token")

def get_current_user(user_id: int = Depends(get_user_id_from_token)) -> dict:
    user = fake_db.get(user_id)
    if not user:
        raise HTTPException(401, "User not found")
    return user

@app.get("/me")
def my_profile(user: dict = Depends(get_current_user)):
    return user
# FastAPI calls: get_token() → get_user_id_from_token() → get_current_user()
# Each depends on the result of the previous

yield Dependencies — Lifecycle Management

from typing import Generator
from sqlalchemy.orm import Session

# ── yield-based dependency: setup → handler → teardown ───────────────────────
def get_db() -> Generator[Session, None, None]:
    db = SessionLocal()
    try:
        yield db          # handler runs here, receiving db
        db.commit()       # commits if no exception
    except Exception:
        db.rollback()     # rolls back if exception raised
        raise
    finally:
        db.close()        # always closes (returns to pool)

# ── Async yield dependency ────────────────────────────────────────────────────
from typing import AsyncGenerator
from sqlalchemy.ext.asyncio import AsyncSession

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

# ── Multiple yield dependencies in one request ────────────────────────────────
# Both run their teardown in LIFO order (last-in, first-out):
# get_db teardown runs before get_redis teardown
@app.get("/data")
def get_data(
    db:    Session     = Depends(get_db),
    redis: Redis       = Depends(get_redis),
    user:  User        = Depends(get_current_user),
):
    ...

Common Mistakes

Mistake 1 — Calling the dependency function directly (not using Depends)

❌ Wrong — bypasses FastAPI’s injection, session lifecycle, caching:

@app.get("/posts")
def list_posts():
    db = get_db()   # calls generator, gets generator object, NOT the session!
    return db.query(Post).all()   # TypeError

✅ Correct — always use Depends():

@app.get("/posts")
def list_posts(db: Session = Depends(get_db)):   # ✓
    return db.scalars(select(Post)).all()

Mistake 2 — Database work after yield in get_db

❌ Wrong — db.close() runs after handler returns; any DB work after close fails:

def get_db():
    db = SessionLocal()
    yield db
    db.commit()   # Works
    result = db.query(Post).count()   # After commit: may use closed connection

✅ Correct — do all database work in the handler (before the yield returns control).

Mistake 3 — Not catching exceptions in yield dependency teardown

❌ Wrong — exception in handler bypasses commit/rollback:

def get_db():
    db = SessionLocal()
    yield db
    db.commit()   # skipped if handler raised an exception!

✅ Correct — use try/except/finally to ensure teardown always runs.

Quick Reference

Pattern Code
Simple dependency param = Depends(function)
Disable caching Depends(fn, use_cache=False)
Yield dependency def dep(): setup(); yield value; teardown()
Sub-dependency Dependency function declares its own Depends()
Router-level dep APIRouter(dependencies=[Depends(require_auth)])
App-level dep app.include_router(router, dependencies=[Depends(rate_limit)])

🧠 Test Yourself

Two route handlers in the same request both declare db: Session = Depends(get_db). How many times does FastAPI call get_db()?