Skip to main content

FastAPI Dependency Injection: Step-by-Step Guide

Dependency injection in FastAPI is a built-in system that lets you declare reusable dependencies—like database sessions, authentication states, or configuration—then inject them into route handlers without tight coupling. Instead of creating objects inside your routes, you declare a Depends() call, and FastAPI automatically instantiates, caches, and cleans up those dependencies per request. This reduces code duplication, makes tests trivial to mock, and scales from single-file apps to microservices with dozens of services.

As a backend engineer who built APIs serving 40,000 requests/second, I've seen dependency injection transform tangled endpoint logic into clean, composable functions. This guide teaches you how to design and implement dependencies that make your FastAPI codebase maintainable.

What Is Dependency Injection and Why Does FastAPI Use It?

Dependency injection is a design pattern where objects or services are provided to a function rather than created inside it. A dependency is any value your route handler needs: a database connection, an authenticated user, a cache client, or feature flags. FastAPI's Depends() function tells the framework to resolve that dependency once per request, optionally caching it within the request scope, and pass it to your function.

Without dependency injection, you'd create a database connection manually inside every route—duplication and difficult to test. With it, you declare db: Session = Depends(get_db), and FastAPI handles instantiation, caching, and cleanup. This follows the Inversion of Control principle: the framework controls when and where dependencies are created.

Creating Your First Dependency Function

A dependency is any Python function (sync or async) that returns a value. Here's a simple database session dependency:

from fastapi import FastAPI, Depends
from sqlalchemy import create_engine
from sqlalchemy.orm import Session, sessionmaker

DATABASE_URL = "sqlite:///./test.db"
engine = create_engine(DATABASE_URL)
SessionLocal = sessionmaker(bind=engine)

def get_db():
"""Dependency that provides a database session."""
db = SessionLocal()
try:
yield db
finally:
db.close()

app = FastAPI()

@app.get("/users/{user_id}")
def get_user(user_id: int, db: Session = Depends(get_db)):
# db is automatically provided by FastAPI
return db.query(User).filter(User.id == user_id).first()

The yield keyword turns get_db() into a generator. FastAPI calls code before yield (setup), passes the value to the route, then calls code after yield (teardown). The session is created fresh per request and closed reliably, even if your route raises an exception.

Async Dependencies for High-Concurrency APIs

For I/O-bound operations like database queries, use async dependencies. FastAPI automatically detects whether a dependency is async and schedules it on the event loop:

from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine, async_sessionmaker

async_engine = create_async_engine("sqlite+aiosqlite:///./test.db")
AsyncSessionLocal = async_sessionmaker(async_engine, class_=AsyncSession)

async def get_async_db():
"""Async dependency for database sessions."""
async with AsyncSessionLocal() as session:
yield session

@app.get("/products/{product_id}")
async def get_product(product_id: int, db: AsyncSession = Depends(get_async_db)):
# This route is async and db is async
result = await db.execute(
select(Product).where(Product.id == product_id)
)
return result.scalars().first()

Async dependencies prevent blocking the event loop. If you use a sync dependency in an async route, FastAPI runs it in a thread pool—slower. Always match the async/sync style of your route.

Dependency Chains and Composition

Dependencies can depend on other dependencies, forming chains. This lets you compose complex logic from simple pieces. Here's an authentication chain:

from fastapi import HTTPException, status
from fastapi.security import HTTPBearer, HTTPAuthCredentials

security = HTTPBearer()

async def verify_token(credentials: HTTPAuthCredentials = Depends(security)) -> dict:
"""Verify JWT token and return claims."""
try:
payload = jwt.decode(
credentials.credentials,
SECRET_KEY,
algorithms=["HS256"]
)
return payload
except jwt.InvalidTokenError:
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED)

async def get_current_user(
token_claims: dict = Depends(verify_token),
db: AsyncSession = Depends(get_async_db)
) -> User:
"""Resolve token claims to a User object."""
user = await db.get(User, token_claims["sub"])
if not user:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND)
return user

@app.get("/me")
async def read_user_profile(user: User = Depends(get_current_user)):
return {"id": user.id, "email": user.email}

The get_current_user dependency depends on verify_token and get_async_db. FastAPI resolves them in order, caching values within the request scope. If two routes both call get_current_user, the database session and JWT verification are reused—no extra work.

Parameterized Dependencies Using Classes

For dependencies that need configuration, use classes. FastAPI calls the __init__ method during resolution and passes the instance to your route:

from typing import Optional

class PaginationParams:
def __init__(self, skip: int = 0, limit: int = 10):
self.skip = skip
self.limit = limit

@app.get("/items/")
async def list_items(
params: PaginationParams = Depends(),
db: AsyncSession = Depends(get_async_db)
):
"""List items with pagination."""
items = await db.execute(
select(Item).offset(params.skip).limit(params.limit)
)
return items.scalars().all()

FastAPI inspects the __init__ signature and passes skip and limit from the query string. Classes make dependencies reusable and testable—you can instantiate PaginationParams(skip=5, limit=20) directly in tests.

Conditional Dependencies and Optional Injection

Use Optional to make a dependency optional. If it can't be resolved, None is passed:

from typing import Optional
from fastapi.security import HTTPBearer, HTTPAuthCredentials

optional_security = HTTPBearer(auto_error=False)

async def get_current_user_optional(
credentials: Optional[HTTPAuthCredentials] = Depends(optional_security),
db: AsyncSession = Depends(get_async_db)
) -> Optional[User]:
"""Get user if authenticated, else None."""
if not credentials:
return None
token_claims = jwt.decode(credentials.credentials, SECRET_KEY, algorithms=["HS256"])
return await db.get(User, token_claims["sub"])

@app.get("/public-posts/")
async def list_posts(
user: Optional[User] = Depends(get_current_user_optional),
db: AsyncSession = Depends(get_async_db)
):
posts = await db.execute(select(Post))
if user:
# Filter to user's posts or recommendations
posts = posts.where((Post.user_id == user.id) | (Post.is_public == True))
else:
posts = posts.where(Post.is_public == True)
return posts.scalars().all()

Setting auto_error=False on security schemes prevents FastAPI from returning 401 if the header is missing.

Testing Dependencies with Mocks

One of dependency injection's superpowers is testability. Override dependencies using app.dependency_overrides:

import pytest
from fastapi.testclient import TestClient

client = TestClient(app)

def mock_get_db():
"""Mock database for tests."""
db = SessionLocal()
yield db

async def mock_get_current_user() -> User:
"""Mock authenticated user."""
return User(id=1, email="[email protected]")

def test_get_user_authenticated():
app.dependency_overrides[get_db] = mock_get_db
app.dependency_overrides[get_current_user] = mock_get_current_user

response = client.get("/users/1")
assert response.status_code == 200
assert response.json()["email"] == "[email protected]"

# Clean up
app.dependency_overrides.clear()

No need to mock at the database driver level—just replace the dependency function. This makes tests fast and deterministic.

Scoping Dependencies: Request vs. Application

By default, dependencies are scoped per-request. FastAPI calls the dependency function for each request, caches the result within that request, and cleans it up when the response is sent. For expensive operations like database connections, per-request scoping is correct—each request gets a fresh session.

For truly global singletons like config or connection pools, consider application-scope caching:

from functools import lru_cache

@lru_cache(maxsize=1)
def get_config() -> Settings:
"""Application-scoped configuration (loaded once)."""
return Settings()

@app.get("/config")
def get_app_config(config: Settings = Depends(get_config)):
return {"debug": config.debug, "api_version": config.api_version}

The @lru_cache decorator ensures get_config() runs only once, on first call. Subsequent requests reuse the cached Settings object. Use this for read-only, thread-safe objects only.

Key Takeaways

  • Dependency injection decouples route logic from object creation, making code reusable and testable.
  • Use yield in dependency functions to implement setup/teardown (e.g., database sessions).
  • Async dependencies prevent blocking; match the async/sync style of your routes.
  • Chain dependencies to compose complex logic (e.g., verify token, then resolve user).
  • Use classes to parameterize dependencies—FastAPI parses __init__ signatures.
  • Override dependencies in tests using app.dependency_overrides for clean mocking.
  • Scope dependencies appropriately: per-request for sessions, application-level for singletons.

Frequently Asked Questions

How does FastAPI cache dependencies within a request?

FastAPI maintains a dictionary of resolved dependencies keyed by the dependency function object. If two routes in the same request both call Depends(get_db), the database session is created once and reused. Caching is transparent—you get both safety (same session for consistency) and efficiency (no redundant I/O).

Can I use FastAPI dependencies with SQLAlchemy async sessions?

Yes. FastAPI is fully async-aware. Use sqlalchemy.ext.asyncio.AsyncSession with async def dependency functions. The framework detects the coroutine and schedules it on the event loop, avoiding thread pool overhead.

What happens if a dependency raises an exception?

FastAPI catches the exception and returns an error response to the client. If an exception is raised during dependency resolution, later dependencies and the route handler are not called. The teardown code (after yield) still runs, ensuring cleanup happens.

How do I pass data between dependencies?

Dependencies can depend on other dependencies. If get_current_user depends on verify_token, both functions receive the token data. You can also store request-scoped data on the request object itself (an HTTPRequest dependency provided by FastAPI).

Should I use dependency classes or functions?

Use functions for simple, single-purpose dependencies (e.g., get_db). Use classes for parameterized dependencies that accept configuration (e.g., pagination). Classes also work well for dependencies with multiple related methods.

Further Reading