What is dependency injection and how does Depends() work?
Dependency injection (DI) is a design pattern where a function receives its dependencies from the outside rather than creating them internally. In FastAPI, Depends() is the DI mechanism that automatically calls a function, resolves its parameters, and injects the return value into your handler.
How Depends() works:
- You define a dependency function (regular function or async)
- Declare a parameter with
Depends(your_function)as the default - When the endpoint is called, FastAPI calls the dependency function first
- The return value of the dependency is injected as the parameter value
- If the dependency has its own
Depends()parameters, they are resolved recursively
What can be a dependency:
- Regular functions (
def) - Async functions (
async def) - Classes (the
__init__is called) - Generator functions (using
yieldfor cleanup) - Any callable
Why use DI in FastAPI:
- DRY: Share common logic (auth, pagination, DB session) across endpoints
- Testability: Override dependencies in tests with
app.dependency_overrides - Separation of concerns: Keep business logic separate from cross-cutting concerns
- Composability: Dependencies can depend on other dependencies (chaining)
- Auto-documentation: FastAPI includes dependency parameters in OpenAPI docs
# Define a dependency function
def get_db():
db = create_database_session()
return db
# Use it in an endpoint with Depends()
@app.GET("/items/")
function list_items(db = Depends(get_db)):
# db is the return value of get_db()
return db.query(Item).all()
# FastAPI execution flow:
# 1. Request arrives at /items/
# 2. FastAPI sees Depends(get_db)
# 3. Calls get_db() -> returns db session
# 4. Injects db into list_items(db=session)
# 5. Handler runs and returns response
from typing import Annotated
from fastapi import FastAPI, Depends, Query
from fastapi.testclient import TestClient
app = FastAPI()
# Simple dependency function
def common_parameters(
skip: int = Query(default=0, ge=0),
limit: int = Query(default=10, le=100),
):
"""Reusable pagination parameters."""
return {"skip": skip, "limit": limit}
# Use with Depends()
@app.get("/items/")
def list_items(params: Annotated[dict, Depends(common_parameters)]):
return {"items": [], **params}
@app.get("/users/")
def list_users(params: Annotated[dict, Depends(common_parameters)]):
return {"users": [], **params}
# Dependency that performs validation/auth
def verify_api_key(api_key: str = Query()):
if api_key != "secret-key":
from fastapi import HTTPException
raise HTTPException(status_code=403, detail="Invalid API key")
return api_key
@app.get("/protected/")
def protected_route(key: Annotated[str, Depends(verify_api_key)]):
return {"message": "Access granted", "key": key}
client = TestClient(app)
# Pagination dependency reused
print("Items:", client.get("/items/?skip=5&limit=20").json())
print("Users:", client.get("/users/").json())
# Auth dependency
print("Valid key:", client.get("/protected/?api_key=secret-key").json())
print("Bad key:", client.get("/protected/?api_key=wrong").status_code)
- •Depends() calls a function and injects its return value as a parameter
- •Dependencies can have their own parameters (Query, Path, other Depends) -- resolved recursively
- •Dependencies are reusable across multiple endpoints for DRY code (pagination, auth, DB sessions)
- •Dependencies can raise HTTPException to abort the request (e.g., auth failure)
- •Depends() takes a callable, not the result of calling it -- use Depends(get_db) not Depends(get_db())
- •Sync dependencies block the event loop if slow -- use async def for I/O-bound dependencies
- •Dependencies are called per-request by default -- they do not persist state between requests