Topic 7
Dependency Injection
Dependency Injection (DI) is one of FastAPI's most powerful features. Instead of hardcoding services, databases, or config inside each route, you declare what you need and FastAPI automatically wires it up. This keeps your code clean, testable, and modular.
Think of it like this: When a restaurant gets an order, the waiter doesn't cook the food — they depend on the kitchen. The waiter just declares what's needed and the kitchen provides it. DI works exactly the same way in FastAPI.
7.1
Depends
Function Dependencies
▾
A function dependency is simply a regular Python function that FastAPI will call automatically before your route handler runs. You tell FastAPI about it using
Depends().
Key idea: The dependency function can accept parameters just like a route — path params, query params, headers — and FastAPI resolves them too.
Request arrives
│
▼
FastAPI sees Depends(get_current_user)
│
▼
Calls get_current_user() first ◄── resolves its own params
│ returns: User object
▼
Route handler receives User as argument
│
▼
Response sent
Example 1 — Basic function dependency (query param validation):
from fastapi import FastAPI, Depends, Query, HTTPException app = FastAPI() # ---- The Dependency ---- def common_pagination( skip: int = Query(0, ge=0), # must be >= 0 limit: int = Query(10, ge=1, le=100) # between 1 and 100 ) -> dict: return {"skip": skip, "limit": limit} # ---- Routes that USE the dependency ---- @app.get("/users") def get_users(pagination: dict = Depends(common_pagination)): # FastAPI called common_pagination() for us and passed the result skip = pagination["skip"] limit = pagination["limit"] return {"skip": skip, "limit": limit, "data": ["user1", "user2"]} @app.get("/products") def get_products(pagination: dict = Depends(common_pagination)): # Same dependency reused — DRY principle! return {"skip": pagination["skip"], "data": ["prod1"]}
Example 2 — Authentication dependency (header check):
from fastapi import FastAPI, Depends, Header, HTTPException from typing import Annotated app = FastAPI() # ---- Auth Dependency ---- def verify_api_key(x_api_key: Annotated[str, Header()]): # FastAPI reads the X-API-Key header automatically if x_api_key != "secret-key-123": raise HTTPException(status_code=403, detail="Invalid API Key") return x_api_key # returned value injected into route # ---- Protected Route ---- @app.get("/secret-data") def secret_data(api_key: Annotated[str, Depends(verify_api_key)]): return {"message": "You are authorized!", "key": api_key} # Test: curl -H "X-API-Key: secret-key-123" http://localhost:8000/secret-data
Note: If a dependency raises an
HTTPException, FastAPI short-circuits — the route handler is never called. This is perfect for auth guards.Example 3 — Async dependency (DB query simulation):
from fastapi import FastAPI, Depends import asyncio app = FastAPI() # ---- Async dependency ---- async def get_db_config() -> dict: await asyncio.sleep(0) # simulate async DB call return {"host": "localhost", "db": "myapp"} # ---- Route using async dependency ---- @app.get("/db-info") async def db_info(config: dict = Depends(get_db_config)): return {"connected_to": config["host"], "db": config["db"]}
Class Dependencies
▾
Sometimes you want a dependency with state or configuration. You can use a class as a dependency — FastAPI calls its
__init__ and passes the instance to your route. This is great for building reusable, configurable services.
Example 1 — Class as a dependency:
from fastapi import FastAPI, Depends app = FastAPI() # ---- Class-based dependency ---- class PaginationParams: def __init__(self, skip: int = 0, limit: int = 10): self.skip = skip self.limit = limit @app.get("/items") def get_items(params: PaginationParams = Depends(PaginationParams)): # params is an instance of PaginationParams return { "skip": params.skip, "limit": params.limit, "items": ["a", "b", "c"] } # GET /items?skip=20&limit=5 → {"skip": 20, "limit": 5, "items": [...]} # Notice: Depends(PaginationParams) is same as Depends(PaginationParams.__init__)
Example 2 — Configurable class dependency (parametrized):
from fastapi import FastAPI, Depends, HTTPException app = FastAPI() # ---- A "factory" class that creates dependencies with config ---- class RoleChecker: def __init__(self, allowed_roles: list[str]): self.allowed_roles = allowed_roles def __call__(self, current_user_role: str = "viewer") -> bool: # __call__ makes the instance callable → FastAPI uses it as a dependency if current_user_role not in self.allowed_roles: raise HTTPException( status_code=403, detail=f"Role '{current_user_role}' not permitted. Need: {self.allowed_roles}" ) return True # ---- Create role-specific dependency instances ---- admin_only = RoleChecker(allowed_roles=["admin"]) editor_or_admin = RoleChecker(allowed_roles=["admin", "editor"]) @app.get("/admin-panel") def admin_panel(_=Depends(admin_only)): return {"message": "Welcome, Admin!"} @app.get("/editor-panel") def editor_panel(_=Depends(editor_or_admin)): return {"message": "Welcome, Editor or Admin!"}
Shortcut syntax:
Depends(MyClass) is identical to Depends(MyClass.__init__). FastAPI will instantiate the class and inject the instance.
Example 3 — Class dependency with
__call__ (callable instances):from fastapi import FastAPI, Depends app = FastAPI() class Logger: def __init__(self, prefix: str): self.prefix = prefix self.logs = [] def __call__(self, message: str) -> "Logger": self.logs.append(f"[{self.prefix}] {message}") return self # Create instance once (shared state) request_logger = Logger(prefix="REQUEST") @app.get("/log-test") def log_test(logger: Logger = Depends(request_logger)): logger(message="Someone hit /log-test") return {"logs": logger.logs}
| Type | When to use | Example |
|---|---|---|
| Function dep | Simple, stateless logic | Pagination, token validation |
| Class dep | State, configuration, reusable services | RoleChecker, DB session factory |
| Callable class | Class instances used as functions | Loggers, rate limiters |
7.2
Dependency Hierarchies
Nested Dependencies
▾
Dependencies can depend on other dependencies. FastAPI builds a full dependency tree and resolves everything in the right order. This lets you compose complex behaviour from simple building blocks.
Route ──► Depends(get_current_user)
└──► Depends(get_db)
└──► Depends(get_settings)
FastAPI resolves bottom-up:
get_settings first → get_db → get_current_user → Route
Example — 3-level dependency chain:
from fastapi import FastAPI, Depends, HTTPException, Header from typing import Annotated app = FastAPI() # ---- Level 1: Lowest-level dependency (settings) ---- def get_settings() -> dict: return {"secret": "my-secret", "env": "production"} # ---- Level 2: Middle dependency (DB) depends on settings ---- def get_db(settings: dict = Depends(get_settings)): # Uses settings to create DB connection return {"connection": f"db-connected-to-{settings['env']}"} # ---- Level 3: Top-level dependency (auth) depends on DB ---- def get_current_user( authorization: Annotated[str, Header()], db: dict = Depends(get_db) ) -> dict: if authorization != "Bearer valid-token": raise HTTPException(status_code=401, detail="Unauthorized") # In real app: query db for user by token return {"user_id": 1, "name": "Alice", "db": db["connection"]} # ---- Route: just asks for the top-level result ---- @app.get("/profile") def profile(user: dict = Depends(get_current_user)): return {"hello": user["name"], "via": user["db"]} # FastAPI automatically chains: # get_settings → get_db → get_current_user → profile
Deep nesting is fine. FastAPI resolves the entire tree before calling your route. You just declare what you need — it figures out the order.
7.3
Application Architecture
DI enables clean architectural separation. FastAPI projects typically use three layers, each injected as a dependency into the layer above it.
HTTP Request
→
Route (Controller)
→
Service Layer
→
Repository Layer
→
Database
Service Layer
▾
The Service Layer contains your business logic — the rules of your application. It doesn't know about HTTP, and doesn't talk to the DB directly. Routes call the service, and the service calls the repository.
Route receives the HTTP request and extracts data.
Service runs business logic (validation rules, calculations, decisions).
Repository reads/writes from the database.
from fastapi import FastAPI, Depends, HTTPException from pydantic import BaseModel app = FastAPI() # ===== MODELS ===== class UserCreate(BaseModel): name: str email: str age: int # ===== REPOSITORY LAYER (data access) ===== class UserRepository: def __init__(self): # In real app: inject DB session here self._db: dict = {} self._next_id = 1 def find_by_email(self, email: str): for user in self._db.values(): if user["email"] == email: return user return None def create(self, data: dict) -> dict: user = {"id": self._next_id, **data} self._db[self._next_id] = user self._next_id += 1 return user def get_all(self) -> list: return list(self._db.values()) # ===== SERVICE LAYER (business logic) ===== class UserService: def __init__(self, repo: UserRepository = Depends(UserRepository)): self.repo = repo def create_user(self, user_data: UserCreate) -> dict: # Business rule: user must be 18+ if user_data.age < 18: raise HTTPException( status_code=400, detail="User must be at least 18 years old" ) # Business rule: email must be unique existing = self.repo.find_by_email(user_data.email) if existing: raise HTTPException( status_code=409, detail=f"Email {user_data.email} already registered" ) return self.repo.create(user_data.model_dump()) def list_users(self) -> list: return self.repo.get_all() # ===== ROUTES (controller layer) ===== @app.post("/users", status_code=201) def create_user( user_data: UserCreate, service: UserService = Depends(UserService) ): # Route just calls service — no business logic here return service.create_user(user_data) @app.get("/users") def list_users(service: UserService = Depends(UserService)): return service.list_users()
Repository Layer
▾
The Repository Layer is the only place that talks to the database. It hides the details of how data is stored (SQL, NoSQL, in-memory) behind simple methods. This makes your service layer and tests database-agnostic.
from fastapi import FastAPI, Depends from typing import Protocol app = FastAPI() # ---- Define a Repository Protocol (interface) ---- class IProductRepository(Protocol): def get_by_id(self, product_id: int) -> dict | None: ... def save(self, product: dict) -> dict: ... # ---- Concrete implementation (in-memory, swap for SQLAlchemy in prod) ---- class InMemoryProductRepo: def __init__(self): self._store = { 1: {"id": 1, "name": "Laptop", "price": 999.99}, 2: {"id": 2, "name": "Phone", "price": 499.99}, } def get_by_id(self, product_id: int) -> dict | None: return self._store.get(product_id) def save(self, product: dict) -> dict: pid = max(self._store.keys()) + 1 product["id"] = pid self._store[pid] = product return product # ---- Dependency provider ---- def get_product_repo() -> InMemoryProductRepo: return InMemoryProductRepo() # ---- Routes ---- @app.get("/products/{product_id}") def get_product( product_id: int, repo: InMemoryProductRepo = Depends(get_product_repo) ): product = repo.get_by_id(product_id) if not product: from fastapi import HTTPException raise HTTPException(status_code=404, detail="Product not found") return product
Unit of Work
▾
The Unit of Work (UoW) pattern groups multiple repository operations into a single transaction. If any step fails, everything is rolled back. Think of it like a database transaction wrapper that coordinates multiple repositories.
Request → Create Order
├── 1. Deduct inventory (InventoryRepo)
├── 2. Create order record (OrderRepo)
└── 3. Charge payment (PaymentRepo)
If step 2 fails → step 1 is ROLLED BACK (Unit of Work handles this)
from fastapi import FastAPI, Depends, HTTPException from contextlib import contextmanager from pydantic import BaseModel app = FastAPI() # ---- Simulated DB ---- fake_db = { "orders": [], "inventory": {"laptop": 5}, } # ---- Repository classes ---- class OrderRepo: def __init__(self, db): self.db = db def create(self, order): self.db["orders"].append(order) class InventoryRepo: def __init__(self, db): self.db = db def deduct(self, item: str, qty: int): if self.db["inventory"].get(item, 0) < qty: raise ValueError(f"Not enough stock for {item}") self.db["inventory"][item] -= qty # ---- Unit of Work ---- class UnitOfWork: def __init__(self): # All repos share the same db reference self.orders = OrderRepo(fake_db) self.inventory = InventoryRepo(fake_db) self._committed = False def commit(self): self._committed = True print("✅ Transaction committed") def rollback(self): print("❌ Transaction rolled back") # In real SQLAlchemy: session.rollback() # ---- Dependency (generator with cleanup) ---- def get_uow(): uow = UnitOfWork() try: yield uow uow.commit() # only if no exception was raised except Exception: uow.rollback() raise # ---- Request model ---- class OrderRequest(BaseModel): item: str quantity: int # ---- Route ---- @app.post("/orders", status_code=201) def create_order( req: OrderRequest, uow: UnitOfWork = Depends(get_uow) ): try: uow.inventory.deduct(req.item, req.quantity) uow.orders.create({"item": req.item, "qty": req.quantity}) return {"status": "Order placed!", "item": req.item} except ValueError as e: raise HTTPException(status_code=400, detail=str(e)) # GET /orders: # POST /orders {"item": "laptop", "quantity": 2} → places order, deducts inventory # POST /orders {"item": "laptop", "quantity": 99} → 400 error, rolled back
In production with SQLAlchemy: The UoW wraps a
Session. On success → session.commit(). On failure → session.rollback(). The yield dependency ensures this happens automatically.
Real-World Full Architecture Example
▾
This example shows how all three layers work together in a realistic project structure — with proper file separation, DI wired through all layers.
Recommended project structure:
app/ ├── main.py ← FastAPI app, mounts routers ├── routers/ │ └── users.py ← Route handlers (controller layer) ├── services/ │ └── user_service.py ← Business logic (service layer) ├── repositories/ │ └── user_repo.py ← DB access (repository layer) ├── models/ │ └── user.py ← Pydantic models └── database.py ← DB session dependency
database.py — DB session as a dependency:
# database.py from sqlalchemy import create_engine from sqlalchemy.orm import sessionmaker, Session DATABASE_URL = "sqlite:///./app.db" engine = create_engine(DATABASE_URL) SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) def get_db() -> Session: db = SessionLocal() try: yield db # inject into routes/services db.commit() # commit on success except Exception: db.rollback() # rollback on error raise finally: db.close() # always close
repositories/user_repo.py:
# repositories/user_repo.py from fastapi import Depends from sqlalchemy.orm import Session from database import get_db class UserRepository: def __init__(self, db: Session = Depends(get_db)): self.db = db def get_by_id(self, user_id: int): # return self.db.query(User).filter(User.id == user_id).first() return {"id": user_id, "name": "Alice"} # simplified
services/user_service.py:
# services/user_service.py from fastapi import Depends, HTTPException from repositories.user_repo import UserRepository class UserService: def __init__(self, repo: UserRepository = Depends(UserRepository)): self.repo = repo def get_user(self, user_id: int) -> dict: user = self.repo.get_by_id(user_id) if not user: raise HTTPException(status_code=404, detail="User not found") return user
routers/users.py:
# routers/users.py from fastapi import APIRouter, Depends from services.user_service import UserService router = APIRouter(prefix="/users", tags=["users"]) @router.get("/{user_id}") def get_user(user_id: int, service: UserService = Depends(UserService)): # FastAPI auto-resolves: UserService → UserRepository → get_db → Session return service.get_user(user_id)
main.py:
# main.py from fastapi import FastAPI from routers import users app = FastAPI() app.include_router(users.router) # FastAPI DI chain for GET /users/1: # get_db() → UserRepository(db) → UserService(repo) → route(service)
Testing advantage: Because everything is injected, you can override any dependency in tests. Example: replace the real DB with a test DB using
app.dependency_overrides[get_db] = get_test_db — no code changes needed.
# tests/test_users.py from fastapi.testclient import TestClient from main import app from database import get_db def get_test_db(): # Return a test/mock DB session instead yield {"test_session": True} # Override the dependency app.dependency_overrides[get_db] = get_test_db client = TestClient(app) def test_get_user(): response = client.get("/users/1") assert response.status_code == 200 assert response.json()["id"] == 1
📋 Topic 7 Summary
| Concept | What it does | Key syntax |
|---|---|---|
| Function Dependency | Reusable logic injected into routes | Depends(my_func) |
| Class Dependency | Stateful/configurable dependency | Depends(MyClass) |
| Nested Dependencies | Dependencies that depend on other deps | Auto-resolved by FastAPI |
| Shared Dependencies | Same dep called once, result reused | Caching (default on) |
| Generator Dependency | Setup + cleanup around a request | yield instead of return |
| Service Layer | Business logic, no HTTP/DB knowledge | Class injected via Depends |
| Repository Layer | Database access, hides SQL details | Class injected via Depends |
| Unit of Work | Groups ops into one transaction | yield dep with commit/rollback |
✅ Approve this topic to continue to Topic 8: Middleware