Free Ebook cover FastAPI for Beginners: Build a Production-Ready REST API

FastAPI for Beginners: Build a Production-Ready REST API

New course

14 pages

Dependency Injection Patterns in FastAPI

Capítulo 6

Estimated reading time: 9 minutes

+ Exercise

What Dependency Injection Means in FastAPI

Dependency Injection (DI) is a pattern where your route handlers declare what they need (a database session, settings, an authenticated user, parsed query parameters), and FastAPI provides those values automatically. In FastAPI, DI is implemented primarily with Depends.

Instead of manually creating resources inside every endpoint (and duplicating the same code), you extract that logic into dependency functions. This keeps route handlers focused on business logic and makes shared concerns consistent across your API.

Why DI is useful

  • Reuse: One implementation of database session creation, pagination parsing, or auth checks can be shared across many routes.
  • Consistency: The same validation and defaults apply everywhere.
  • Composability: Dependencies can depend on other dependencies.
  • Testability: You can override dependencies in tests to swap real services for fakes.

How Depends Works

A dependency is usually a function (sync or async) whose parameters can themselves be dependencies. FastAPI resolves the dependency graph per request, caches results when appropriate, and injects the returned value into your endpoint.

from fastapi import Depends, FastAPI
app = FastAPI()
def get_config() -> dict:
    return {"feature_x": True}
@app.get("/status")
def status(config: dict = Depends(get_config)):
    return {"ok": True, "feature_x": config["feature_x"]}

FastAPI calls get_config() and passes its return value into status() as config.

Dependency Scopes and Lifetimes

Most dependencies are resolved once per request. Within a single request, FastAPI caches dependency results so that if the same dependency is required multiple times (directly or indirectly), it is executed only once.

Continue in our app.

You can listen to the audiobook with the screen off, receive a free certificate for this course, and also have access to 5,000 other free online courses.

Or continue reading below...
Download App

Download the app

Per-request caching

If get_db() is used by multiple dependencies and the endpoint, FastAPI will typically create one session per request and reuse it.

Yield dependencies for setup/teardown

When you need cleanup (closing a DB session, releasing a connection), use yield. Code after yield runs after the response is produced.

from typing import Generator
def get_resource() -> Generator[str, None, None]:
    resource = "opened"
    try:
        yield resource
    finally:
        resource = "closed"

Request vs application-level state

DI is not a replacement for application startup configuration. Use DI for values that are request-scoped (user, request-specific parsing, DB session) or for abstracting access to app-scoped objects (settings, clients) in a testable way.

Composing Dependencies (Dependencies Depending on Dependencies)

Dependencies compose naturally: a dependency can declare its own dependencies. This is a powerful pattern for authentication and authorization, where “current user” depends on “token extraction,” which depends on “settings,” etc.

from fastapi import Depends
def get_settings() -> dict:
    return {"auth_enabled": True}
def get_token(settings: dict = Depends(get_settings)) -> str | None:
    if not settings["auth_enabled"]:
        return None
    # stub: in real code you'd read Authorization header
    return "fake-token"
def get_current_user(token: str | None = Depends(get_token)) -> dict:
    if token is None:
        return {"id": "anonymous", "role": "guest"}
    return {"id": "user_123", "role": "member"}

Now any endpoint can request current_user without knowing how tokens are obtained.

Reusable Dependency Patterns

1) Database session dependency (get_db)

A common pattern is to create one database session per request and ensure it is closed even if an error occurs.

from typing import Generator
# Assume SessionLocal is your session factory (e.g., from SQLAlchemy)
def get_db() -> Generator["Session", None, None]:
    db = SessionLocal()
    try:
        yield db
    finally:
        db.close()

Usage in a router:

from fastapi import APIRouter, Depends
router = APIRouter(prefix="/items", tags=["items"])
@router.get("/")
def list_items(db = Depends(get_db)):
    # db is a request-scoped session
    return []

2) Configuration/settings dependency

Even if settings are application-scoped, injecting them via a dependency makes it easy to override in tests.

from dataclasses import dataclass
@dataclass(frozen=True)
class Settings:
    page_size_default: int = 20
    page_size_max: int = 100
def get_settings() -> Settings:
    return Settings()

Usage:

def some_endpoint(settings: Settings = Depends(get_settings)):
    return {"default": settings.page_size_default}

3) Common query parsing: pagination parameters

Instead of repeating page/limit parsing in every list endpoint, centralize it. You can return a small object (dict, dataclass, or Pydantic model) representing the parsed parameters.

from dataclasses import dataclass
from fastapi import Depends, Query
@dataclass(frozen=True)
class Pagination:
    offset: int
    limit: int
def pagination_params(
    page: int = Query(1, ge=1),
    page_size: int | None = Query(None, ge=1),
    settings: Settings = Depends(get_settings),
) -> Pagination:
    size = page_size or settings.page_size_default
    size = min(size, settings.page_size_max)
    return Pagination(offset=(page - 1) * size, limit=size)

Usage:

@router.get("/")
def list_items(
    db = Depends(get_db),
    pagination: Pagination = Depends(pagination_params),
):
    # Use pagination.offset and pagination.limit in queries
    return {"offset": pagination.offset, "limit": pagination.limit, "items": []}

4) Authentication check: current_user stub

You often want endpoints to receive a user object that is guaranteed to exist (or raise an error). Here we keep it as a stub to focus on DI patterns.

from dataclasses import dataclass
from fastapi import Depends, HTTPException, status
@dataclass(frozen=True)
class User:
    id: str
    role: str
def current_user() -> User:
    # stub: replace with real auth later
    user = User(id="user_123", role="member")
    if not user:
        raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Not authenticated")
    return user

Usage:

@router.post("/")
def create_item(
    db = Depends(get_db),
    user: User = Depends(current_user),
):
    return {"created_by": user.id}

5) Authorization as a dependency (role check)

Authorization is a great use case for dependency composition: a “require admin” dependency can depend on current_user.

def require_role(required: str):
    def _checker(user: User = Depends(current_user)) -> User:
        if user.role != required:
            raise HTTPException(status_code=403, detail="Forbidden")
        return user
    return _checker
@router.delete("/{item_id}")
def delete_item(
    item_id: str,
    db = Depends(get_db),
    user: User = Depends(require_role("admin")),
):
    return {"deleted": item_id, "by": user.id}

Dependency Injection and Testability

DI improves testability because route handlers don’t create concrete resources directly. Instead, they depend on abstract providers that can be replaced during tests.

Overriding dependencies

FastAPI allows overriding dependencies via app.dependency_overrides. This lets you replace get_db with a fake session, or current_user with a deterministic user.

from fastapi.testclient import TestClient
def fake_get_db():
    class FakeDB:
        def close(self):
            pass
    db = FakeDB()
    try:
        yield db
    finally:
        db.close()
def fake_current_user() -> User:
    return User(id="test_user", role="admin")
app.dependency_overrides[get_db] = fake_get_db
app.dependency_overrides[current_user] = fake_current_user
client = TestClient(app)
def test_delete_item_as_admin():
    r = client.delete("/items/abc")
    assert r.status_code == 200

This approach avoids hitting real databases or implementing real auth in unit tests.

Step-by-Step Refactor: Replace Duplicated Code in Routers with Dependencies

Imagine you have multiple routers with repeated code: creating a DB session, parsing pagination, and checking authentication. The refactor goal is to move those repeated blocks into dependencies and keep endpoints small.

Step 1: Identify duplication

Typical repeated patterns you might see:

  • Creating/closing DB sessions in every endpoint
  • Parsing page/page_size repeatedly
  • Copy/pasting user checks or role checks

Step 2: Create a dependencies.py module

Centralize shared dependencies so routers import them from one place.

# app/dependencies.py
from dataclasses import dataclass
from typing import Generator
from fastapi import Depends, HTTPException, Query
from .settings import Settings, get_settings
from .db import SessionLocal
@dataclass(frozen=True)
class Pagination:
    offset: int
    limit: int
def get_db() -> Generator["Session", None, None]:
    db = SessionLocal()
    try:
        yield db
    finally:
        db.close()
def pagination_params(
    page: int = Query(1, ge=1),
    page_size: int | None = Query(None, ge=1),
    settings: Settings = Depends(get_settings),
) -> Pagination:
    size = page_size or settings.page_size_default
    size = min(size, settings.page_size_max)
    return Pagination(offset=(page - 1) * size, limit=size)
@dataclass(frozen=True)
class User:
    id: str
    role: str
def current_user() -> User:
    user = User(id="user_123", role="member")
    if not user:
        raise HTTPException(status_code=401, detail="Not authenticated")
    return user
def require_role(required: str):
    def _checker(user: User = Depends(current_user)) -> User:
        if user.role != required:
            raise HTTPException(status_code=403, detail="Forbidden")
        return user
    return _checker

Step 3: Refactor routers to use dependencies

Before refactor (duplicated patterns inside endpoints):

# app/routers/items.py
from fastapi import APIRouter, HTTPException, Query
from ..db import SessionLocal
router = APIRouter(prefix="/items", tags=["items"])
@router.get("/")
def list_items(page: int = Query(1, ge=1), page_size: int = Query(20, ge=1)):
    db = SessionLocal()
    try:
        offset = (page - 1) * page_size
        limit = page_size
        return {"offset": offset, "limit": limit, "items": []}
    finally:
        db.close()
@router.post("/")
def create_item():
    db = SessionLocal()
    try:
        # repeated auth stub
        user = {"id": "user_123"}
        if not user:
            raise HTTPException(status_code=401, detail="Not authenticated")
        return {"created_by": user["id"]}
    finally:
        db.close()

After refactor (dependencies injected):

# app/routers/items.py
from fastapi import APIRouter, Depends
from ..dependencies import Pagination, User, current_user, get_db, pagination_params
router = APIRouter(prefix="/items", tags=["items"])
@router.get("/")
def list_items(
    db = Depends(get_db),
    pagination: Pagination = Depends(pagination_params),
):
    return {"offset": pagination.offset, "limit": pagination.limit, "items": []}
@router.post("/")
def create_item(
    db = Depends(get_db),
    user: User = Depends(current_user),
):
    return {"created_by": user.id}

Step 4: Apply the same refactor across other routers

Once dependencies exist, other routers can adopt them with minimal changes. For example, an admin-only router can use require_role("admin") everywhere without repeating checks.

# app/routers/admin.py
from fastapi import APIRouter, Depends
from ..dependencies import User, get_db, require_role
router = APIRouter(prefix="/admin", tags=["admin"])
@router.get("/metrics")
def metrics(
    db = Depends(get_db),
    user: User = Depends(require_role("admin")),
):
    return {"viewer": user.id, "metrics": {"requests": 123}}

Step 5: Keep route handlers thin

A useful rule of thumb is: if multiple endpoints need the same pre-work (open DB, parse query params, load user, enforce role), that pre-work should be a dependency. Endpoints should read like a small orchestration layer that calls your domain logic with already-prepared inputs.

ConcernWhere it belongsResult
DB session lifecycleget_db yield dependencyOne session per request, always closed
Pagination parsingpagination_paramsConsistent defaults and limits
Authenticationcurrent_userEndpoints receive a user object
Authorizationrequire_roleRole checks are reusable and centralized
Testingdependency_overridesSwap real dependencies for fakes

Now answer the exercise about the content:

In FastAPI, what is the main benefit of defining a database session as a yield-based dependency (e.g., get_db) instead of creating and closing the session inside every endpoint?

You are right! Congratulations, now go to the next page

You missed! Try again.

A yield dependency can set up a resource, provide it to the endpoint, and then run teardown code after the response. FastAPI typically resolves and caches the dependency once per request, so the same session can be reused during that request and still be closed reliably.

Next chapter

Background Tasks and Non-Blocking Workflows in FastAPI

Arrow Right Icon
Download the app to earn free Certification and listen to the courses in the background, even with the screen off.