FastAPI Dependency Injection: Build Modular, Test‑Ready Services with Ease
FastAPI’s <code>Depends</code> lets you decouple request handlers from resource acquisition. Learn how to write sync and async dependencies, test them with <code>TestClient</code>, and avoid common pitfalls like deep chains and circular imports.
30 Aug 2025, 02:13 UTC

Problem: Tight Coupling in Path Operations
When a FastAPI endpoint pulls a database session, reads a config file, or opens a network client directly inside the route function, the logic becomes hard to reuse, hard to test, and hard to maintain. Each new endpoint duplicates the same boilerplate, and swapping a database driver for a mock becomes a tedious copy‑paste exercise.
Thesis: Use Depends to Decouple Resources from Request Logic
FastAPI’s Depends decorator turns any callable into a dependency that the router can inject automatically. By moving resource acquisition (DB sessions, configuration objects, external clients) into dedicated dependency functions, you centralise lifecycle management, keep path operations lean, and unlock straightforward unit testing.
Section 1: Synchronous Database Session as a Dependency
Below is a minimal example that shows how a SQLAlchemy session is yielded and closed per request.
# db.py
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker, Session
DATABASE_URL = "sqlite:///./test.db"
engine = create_engine(DATABASE_URL, connect_args={"check_same_thread": False})
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
def get_db() -> Session:
"""Yield a DB session and ensure it is closed after the request."""
db = SessionLocal()
try:
yield db
finally:
db.close()
In the router you inject it like this:
# router.py
from fastapi import APIRouter, Depends
from .db import get_db
from sqlalchemy.orm import Session
router = APIRouter()
@router.get("/items/{item_id}")
def read_item(item_id: int, db: Session = Depends(get_db)):
return db.query(Item).filter(Item.id == item_id).first()
Key points:
get_dbis a generator; theyieldkeyword hands the session to FastAPI and thefinallyblock guarantees cleanup.- FastAPI calls
get_dbonce per request, regardless of how many operations use the same session. - Because the session is injected, you can replace
get_dbwith a mock that returns a stub session during tests.
Section 2: Asynchronous Dependencies for I/O‑Bound Work
FastAPI supports async dependencies, which is essential when using async DB drivers (e.g., asyncpg) or making HTTP calls. Here’s an async config reader that loads a JSON file only once per request.
# config.py
import json
from typing import Dict
async def load_config() -> Dict[str, str]:
with open("config.json", "r") as f:
return json.load(f)
Inject it into a route:
# router.py
@router.get("/config")
async def get_config(cfg: Dict[str, str] = Depends(load_config)):
return cfg
Because the dependency is async, FastAPI awaits it before executing the path operation. The same function can be mocked with an async lambda in tests.
Section 3: Testing with TestClient and Mocked Dependencies
FastAPI’s TestClient from starlette.testclient runs the full dependency chain. To test read_item without a real database, replace get_db with a stub:
# test_router.py
from fastapi import Depends
from fastapi.testclient import TestClient
from .router import router
from .db import get_db
app = FastAPI()
app.include_router(router)
# Stub dependency
async def fake_db():
class FakeSession:
def query(self, *args, **kwargs):
class Query:
def filter(self, *args, **kwargs):
return self
def first(self):
return {"id": 1, "name": "mock"}
return Query()
yield FakeSession()
app.dependency_overrides[get_db] = fake_db
client = TestClient(app)
assert client.get("/items/1").json() == {"id": 1, "name": "mock"}
Notice the app.dependency_overrides dictionary. FastAPI will use fake_db instead of get_db for the duration of the test, making the endpoint fully isolated.
Trade‑Offs and Limitations
- Deep dependency chains can make the control flow hard to follow. Keep dependencies small and avoid nesting more than two levels.
- Circular imports may surface if a dependency imports a module that, in turn, imports the dependency. Use lazy imports or restructure modules.
- Adding
Dependsto trivial helper functions adds a small runtime overhead and can clutter the signature. Reserve dependencies for resource acquisition or logic that benefits from being shared.
Actionable Checklist
- Identify resources that should be shared or cleaned up (DB sessions, config, HTTP clients).
- Write a dependency function that yields or returns the resource.
- Inject it into path operations with
Depends. - In tests, override the dependency with a mock that matches the signature.
- Run
uvicorn app:app --reloadand hit the endpoint to confirm the dependency resolves without errors.
By following this pattern, you’ll build FastAPI services that are modular, testable, and easier to evolve.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.