FastAPI

FastAPI Dependency Injection (Mastering Depends())

Thirdy Gayares
14 min read

🎯 What You Will Learn

Dependency Injection (DI) is the feature you use in every FastAPI project — for auth, database sessions, pagination, config — but it's rarely explained on its own. In this tutorial we fix that, from zero to production habits.

  • What DI actually is and the problem it solves
  • Your first dependency with Depends()
  • The modern Annotated style (recommended)
  • Sub-dependencies (dependencies that use other dependencies)
  • yield dependencies for setup + cleanup (DB sessions)
  • Class-based dependencies for reusable parameters
  • Router-level and app-level dependencies
  • Common mistakes and how to avoid them

Prerequisites: Python 3.11+, and you should already be comfortable creating a basic FastAPI app and a Pydantic model. If not, start with set-up-python-fast-api and python-fast-api-schema first.

fastapi-di/
├── requirements.txt
└── app/
    ├── __init__.py
    ├── main.py         # routes + wiring
    ├── dependencies.py # our reusable dependencies
    └── db.py           # fake database + session

What Is Dependency Injection (Plain Version)

A dependency is just something your endpoint needs before it can run: a database session, the current logged-in user, validated query parameters. Dependency injection means FastAPI gives those things to your function instead of you building them inside every handler.

You declare what you need with Depends(...), and FastAPI does three things for you: it runs the dependency, passes the result into your function, and shows it in the Swagger docs automatically.

💡
Ang importante dito: a dependency is any callable (function or class) that returns a value. FastAPI calls it for you, per request, and injects the return value where you asked.

The Problem DI Solves (See It First)

Imagine two endpoints that both need the same pagination logic. Without DI, you copy-paste the same code and the same validation into every route:

app/main.py (without DI — repetitive)
from fastapi import FastAPI, HTTPException, Query

app = FastAPI()

@app.get("/users")
def list_users(page: int = Query(1), size: int = Query(10)):
    if page < 1 or size < 1 or size > 100:
        raise HTTPException(status_code=400, detail="Bad pagination")
    offset = (page - 1) * size
    return {"page": page, "size": size, "offset": offset, "items": []}

@app.get("/orders")
def list_orders(page: int = Query(1), size: int = Query(10)):
    # exact same validation copy-pasted... 😩
    if page < 1 or size < 1 or size > 100:
        raise HTTPException(status_code=400, detail="Bad pagination")
    offset = (page - 1) * size
    return {"page": page, "size": size, "offset": offset, "items": []}
⚠️
Why this hurts: the validation is duplicated. Fix a bug in one place and you'll forget the other. This is exactly the kind of shared logic DI is built to extract.

Your First Dependency with Depends()

Move the pagination logic into a single function. That function becomes a dependency. Both endpoints just ask for it:

app/dependencies.py
from fastapi import HTTPException, Query


def pagination_params(page: int = Query(1), size: int = Query(10)) -> dict[str, int]:
    if page < 1 or size < 1 or size > 100:
        raise HTTPException(status_code=400, detail="Bad pagination")
    offset = (page - 1) * size
    return {"page": page, "size": size, "offset": offset}
app/main.py
from fastapi import Depends, FastAPI

from app.dependencies import pagination_params

app = FastAPI(title="FastAPI DI", version="0.1.0")


@app.get("/users")
def list_users(pagination: dict = Depends(pagination_params)):
    return {**pagination, "items": []}


@app.get("/orders")
def list_orders(pagination: dict = Depends(pagination_params)):
    return {**pagination, "items": []}
One source of truth: the validation now lives in pagination_params. Both routes reuse it, and the page / size query params still show up in Swagger automatically.
💡
Notice we never call the function ourselves — we pass the reference: Depends(pagination_params), not Depends(pagination_params()). FastAPI calls it for you on each request.

The Modern Annotated Style (Recommended)

The = Depends(...) form works, but the FastAPI-recommended way is Annotated. It lets you define the dependency type once and reuse it everywhere — cleaner signatures, no repetition.

app/dependencies.py
from typing import Annotated

from fastapi import Depends, HTTPException, Query


def pagination_params(page: int = Query(1), size: int = Query(10)) -> dict[str, int]:
    if page < 1 or size < 1 or size > 100:
        raise HTTPException(status_code=400, detail="Bad pagination")
    offset = (page - 1) * size
    return {"page": page, "size": size, "offset": offset}


# Define the injectable type ONCE, reuse it in every route.
Pagination = Annotated[dict, Depends(pagination_params)]
app/main.py
from fastapi import FastAPI

from app.dependencies import Pagination

app = FastAPI(title="FastAPI DI", version="0.1.0")


@app.get("/users")
def list_users(pagination: Pagination):
    return {**pagination, "items": []}


@app.get("/orders")
def list_orders(pagination: Pagination):
    return {**pagination, "items": []}
StyleLooks likeWhen to use
= Depends()param: dict = Depends(fn)Fine, still supported everywhere
Annotatedparam: Annotated[dict, Depends(fn)]Recommended — define once, reuse, cleaner
💡
House style for this blog: we use Annotated from here on. It's the same style FastAPI uses for Session and current-user dependencies in real projects.

Sub-Dependencies (Dependencies That Use Dependencies)

Dependencies can depend on other dependencies — FastAPI resolves the whole chain for you. This is how real auth works: read a token → decode it → load the user → check the role.

app/dependencies.py
from typing import Annotated

from fastapi import Depends, Header, HTTPException

# Pretend token -> user mapping (dummy data for the demo).
FAKE_TOKENS = {
    "token-admin": {"id": 1, "username": "thirdy", "role": "admin"},
    "token-user": {"id": 2, "username": "maria", "role": "user"},
}


def get_token(authorization: str = Header(...)) -> str:
    # Expect header: "Authorization: Bearer <token>"
    if not authorization.startswith("Bearer "):
        raise HTTPException(status_code=401, detail="Missing Bearer token")
    return authorization.removeprefix("Bearer ")


def get_current_user(token: Annotated[str, Depends(get_token)]) -> dict:
    user = FAKE_TOKENS.get(token)
    if user is None:
        raise HTTPException(status_code=401, detail="Invalid token")
    return user


def require_admin(user: Annotated[dict, Depends(get_current_user)]) -> dict:
    if user["role"] != "admin":
        raise HTTPException(status_code=403, detail="Admin only")
    return user


CurrentUser = Annotated[dict, Depends(get_current_user)]
AdminUser = Annotated[dict, Depends(require_admin)]
app/main.py
from fastapi import FastAPI

from app.dependencies import AdminUser, CurrentUser

app = FastAPI(title="FastAPI DI", version="0.1.0")


@app.get("/me")
def me(user: CurrentUser):
    return {"you_are": user["username"], "role": user["role"]}


@app.get("/admin/reports")
def admin_reports(admin: AdminUser):
    return {"message": f"Welcome admin {admin['username']}", "reports": []}

The chain for /admin/reports is: get_tokenget_current_user require_admin. You only wrote small, single-purpose functions and FastAPI stitched them together.

💡
Caching win: within a single request, FastAPI calls each dependency once and reuses the result. So even if get_current_user appears in three places, the token is decoded only once.

Yield Dependencies for Setup + Cleanup (DB Sessions)

The most important production pattern: a dependency that opens a resource, hands it to your endpoint, then always cleans up afterwards. You do this with yield instead of return.

app/db.py
class FakeSession:
    """Stand-in for a real SQLAlchemy/SQLModel session."""

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

    def query_users(self) -> list[dict]:
        return [{"id": 1, "username": "thirdy"}, {"id": 2, "username": "maria"}]

    def close(self) -> None:
        self.closed = True
        print("[db] session closed")
app/dependencies.py
from typing import Annotated
from collections.abc import Generator

from fastapi import Depends

from app.db import FakeSession


def get_session() -> Generator[FakeSession, None, None]:
    session = FakeSession()   # setup: runs before the endpoint
    try:
        yield session         # this value is injected into the route
    finally:
        session.close()       # teardown: runs AFTER the response is sent


SessionDep = Annotated[FakeSession, Depends(get_session)]
app/main.py
from fastapi import FastAPI

from app.dependencies import SessionDep

app = FastAPI(title="FastAPI DI", version="0.1.0")


@app.get("/db/users")
def db_users(session: SessionDep):
    return {"items": session.query_users()}

Code before yield runs on the way in; code in the finally block runs on the way out — even if your endpoint raised an exception. That's why the session is guaranteed to close.

This is THE database pattern. In a real project get_session yields a SQLModel/ SQLAlchemy Session, and every route that touches the DB just declares session: SessionDep. No manual open/close, no leaks.
⚠️
Gotcha: always wrap the yield in try/finally. If you clean up withoutfinally, an exception in the endpoint skips your cleanup and leaks the connection.

Class-Based Dependencies

When a dependency needs to group several related parameters (like filters), a class is cleaner than a function. A class is callable, so FastAPI treats Depends(MyClass) as "call the constructor with the request's query params."

app/dependencies.py
from typing import Annotated

from fastapi import Depends


class UserFilters:
    def __init__(self, q: str | None = None, role: str | None = None, active: bool = True) -> None:
        self.q = q
        self.role = role
        self.active = active


Filters = Annotated[UserFilters, Depends(UserFilters)]
app/main.py
from fastapi import FastAPI

from app.dependencies import Filters

app = FastAPI(title="FastAPI DI", version="0.1.0")


@app.get("/search")
def search_users(filters: Filters):
    return {
        "search": filters.q,
        "role": filters.role,
        "active_only": filters.active,
    }
💡
Why a class? You get real attribute access (filters.role) with editor autocomplete, and the constructor params become documented query parameters automatically.

Router-Level and App-Level Dependencies

Sometimes a dependency must run on every route in a group — for example, requiring auth on an entire admin router. You attach it once with dependencies=[...] instead of adding it to each handler.

app/main.py
from fastapi import APIRouter, Depends, FastAPI

from app.dependencies import require_admin

app = FastAPI(title="FastAPI DI", version="0.1.0")

# Every route in this router requires an admin. No per-route repetition.
admin_router = APIRouter(
    prefix="/admin",
    tags=["admin"],
    dependencies=[Depends(require_admin)],
)


@admin_router.get("/stats")
def stats():
    return {"users": 2, "orders": 5}


@admin_router.get("/settings")
def settings():
    return {"maintenance": False}


app.include_router(admin_router)
LevelHowUse it for
Per-routeparam: CurrentUserWhen you need the returned value inside the handler
RouterAPIRouter(dependencies=[...])A guard for a whole group (admin, internal APIs)
AppFastAPI(dependencies=[...])Something that must run on every request (rare)
💡
Use dependencies=[...] (in the decorator/router) when you only need the side effect — like "must be admin" — and don't need the return value. Use a parameter when you need the value.

Try It in Swagger (/docs)

Install, run, and test. Every dependency's parameters show up in the docs automatically.

requirements.txt
fastapi==0.116.1
uvicorn[standard]==0.30.6
1Install & run
run.sh
python -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
uvicorn app.main:app --reload
2Open the docs and test the chain

Open http://127.0.0.1:8000/docs. Call GET /me with header Authorization: Bearer token-user, then try GET /admin/reports with the same token to see the 403, then switch to token-admin.

# valid user
$ curl -H "Authorization: Bearer token-user" http://127.0.0.1:8000/me
{"you_are":"maria","role":"user"}

# non-admin hitting an admin route
$ curl -H "Authorization: Bearer token-user" http://127.0.0.1:8000/admin/reports
{"detail":"Admin only"}

# admin hitting the same route
$ curl -H "Authorization: Bearer token-admin" http://127.0.0.1:8000/admin/reports
{"message":"Welcome admin thirdy","reports":[]}

# yield dependency: watch the server log after the response
$ curl http://127.0.0.1:8000/db/users
{"items":[{"id":1,"username":"thirdy"},{"id":2,"username":"maria"}]}
# server console prints: [db] session closed
If the admin guard returns 403 for token-user and 200 for token-admin, and you see [db] session closed in the logs, your DI is working end to end.

Common Mistakes (and Fixes)

MistakeSymptomFix
Calling the dependency: Depends(fn())TypeError / dependency runs at import timePass the reference: Depends(fn)
Using return where cleanup is neededDB connections never close (leaks)Use yield inside try/finally
Putting business logic in a route that another route also needsCopy-paste drift and inconsistent validationExtract it into a dependency
Needing the value but using dependencies=[...]Value not available in the handlerDeclare it as a typed parameter instead
Expecting a dependency to run multiple times per requestConfused by cached resultsRemember: same dependency is resolved once per request (use_cache)
⚠️
Production note: keep dependencies focused and cheap. A dependency that does a slow network call runs on every matching request — cache it (lru_cache for config) or make it async so it doesn't block the event loop.

What's Next

You now understand the single most-used FastAPI feature. Here's where it pays off next:

🚀 Recommended next reads:
  • fastapi-error-handling — turn those HTTPExceptions into clean, global error handlers
  • fastapi-jwt — replace the fake token map with real access/refresh tokens (same DI pattern)
  • fastapi-sqlmodel — swap FakeSession for a real database session via get_session
  • fastapi-pydantic-settings — inject typed config as a cached dependency

Recap: a dependency is any callable FastAPI runs for you and injects. Master Depends(), the Annotated style, sub-dependencies, and yield cleanup — and the rest of FastAPI (auth, DB, config) becomes just "another dependency."

About the Author

TG

Thirdy Gayares

Passionate developer creating custom solutions for everyone. I specialize in building user-friendly tools that solve real-world problems while maintaining the highest standards of security and privacy.