60.4 Dependency Injection System
Right, so we’ve arrived at one of FastAPI’s killer features: its Dependency Injection (DI) system. Don’t let the fancy term scare you. All it really means is that instead of a function having to go out and find the things it needs (like a database session or the current user), you, the all-powerful developer, declare those needs upfront. FastAPI then makes sure they’re delivered, like a well-organized butler who knows exactly what you need before you even ask. It’s the architectural pattern that keeps your code from turning into a tangled mess of manual labor.
The beauty here is that it works exactly the same way for both your path operation functions (the ones with the @app.get("/") decorators) and your background tasks, your APIRouters, and even other dependencies. The consistency is frankly brilliant.
The Basic Pattern: It’s Just a Function
At its heart, a dependency is just a function. Or, more precisely, anything callable. But we’ll start with functions because they’re the easiest to understand. You define a function that returns the value you need. Then, you tell FastAPI, “Hey, for this path operation, I need the result of that function.”
Let’s say you need to get a common query parameter from multiple endpoints. Doing it manually in every function is a recipe for typos and boredom.
from fastapi import FastAPI, Depends
app = FastAPI()
# This is our dependency function.
def common_parameters(q: str | None = None, skip: int = 0, limit: int = 100):
# It returns a dict of the common parameters. Simple.
return {"q": q, "skip": skip, "limit": limit}
@app.get("/items/")
# See that? We're 'depending' on the result of `common_parameters`
async def read_items(commons: dict = Depends(common_parameters)):
# FastAPI calls `common_parameters()` and passes the result as `commons`
return commons
@app.get("/users/")
async def read_users(commons: dict = Depends(common_parameters)):
return commons
Hit /items/?q=test&skip=10&limit=20 and you’ll get {"q": "test", "skip": 10, "limit": 20} back. Clean, reusable, and testable. You can now mock common_parameters in your tests without touching a network request.
Leveling Up: Classes as Dependencies
While functions are great, sometimes you need the structure and validation power of a Pydantic model. This is where using a class as a dependency shines. It feels like black magic the first time you see it.
from fastapi import FastAPI, Depends
from pydantic import BaseModel
app = FastAPI()
# Define a Pydantic model for your dependency
class ItemQueryParams:
def __init__(self, q: str | None = None, skip: int = 0, limit: int = 100):
self.q = q
self.skip = skip
self.limit = limit
# Now use it
@app.get("/items/")
async def read_items(commons: ItemQueryParams = Depends(ItemQueryParams)):
# Now `commons` is an actual instance of `ItemQueryParams`!
return {"message": f"Searching for {commons.q}, skipping {commons.skip} items"}
Wait, what? How does FastAPI know how to call my __init__ method? It uses the same parameter analysis magic it uses for your path operations. It reads the function signature (in this case, the class constructor’s signature) and automatically handles the request parsing for you. It’s frankly a bit absurd how well this works.
The Real Power: Chaining and Sharing
This is where you go from “oh, that’s neat” to “I will never build an API without this again.” Dependencies can depend on other dependencies. This is your primary tool for managing shared resources and application state.
The most classic example? Getting a database session.
from fastapi import FastAPI, Depends, HTTPException
from sqlalchemy.orm import Session
# Imagine we've set this up elsewhere
from .database import get_db
app = FastAPI()
def get_current_user(db: Session = Depends(get_db)):
# Some logic to get the user from a token in the request headers
user = ... # fake user logic
if not user:
raise HTTPException(status_code=401, detail="Invalid credentials")
return user
@app.get("/users/me")
async def read_current_user(current_user: User = Depends(get_current_user)):
return current_user
@app.post("/users/me/items")
async def create_item_for_user(
item: ItemCreate,
current_user: User = Depends(get_current_user),
db: Session = Depends(get_db)
):
# Both `current_user` and `db` are resolved automatically.
# The best part? The `get_db` dependency for the session is only called ONCE per request,
# even though it's used in two different places (`get_current_user` and here).
# FastAPI caches the result of a dependency within a request by default. Smart.
new_item = create_user_item(db=db, item=item, user_id=current_user.id)
return new_item
See the elegance? get_current_user depends on get_db. The path operation depends on both get_current_user and get_db. The dependency tree is resolved from the bottom up, cleanly and efficiently. This is how you avoid passing a db session through ten layers of function calls manually.
Pitfalls and The yield Trick
Here’s the first rough edge. What if your dependency needs cleanup logic? Opening a database connection is the textbook example. You need to close it when the request is done. If you use a normal function with return, you have no hook to run cleanup.
The solution? Make your dependency a generator function using yield instead of return. FastAPI is smart enough to run the code after the yield as the cleanup step.
from fastapi import FastAPI, Depends
from .database import SessionLocal
app = FastAPI()
# Correct way: using yield
def get_db():
db = SessionLocal()
try:
yield db # FastAPI will pass this value to the path operation
finally:
db.close() # This runs after the response is sent. Perfect for cleanup.
# DANGER: Wrong way without cleanup
def get_db_bad():
return SessionLocal() # Who closes this? Nobody. You have a connection leak.
This yield pattern is non-negotiable for any resource that needs cleanup. Forget it, and you’ll be chasing down leaks at 2 AM. Consider this your official warning.
Overrides: For Testing and Evil
The final piece of the puzzle is the ability to override dependencies. This is almost exclusively for testing, and it’s a godsend. In your tests, you don’t want to call the real get_current_user that checks a JWT token; you want a mock user. FastAPI lets you override any dependency for a specific app.
from fastapi.testclient import TestClient
from .main import app, get_current_user
# Client for testing
client = TestClient(app)
def mock_current_user():
return User(id=1, username="testuser")
# Override the real dependency with our mock for this test
app.dependency_overrides[get_current_user] = mock_current_user
def test_read_current_user():
response = client.get("/users/me")
assert response.status_code == 200
assert response.json()["username"] == "testuser"
# Don't forget to clear overrides afterward to avoid polluting other tests!
app.dependency_overrides.clear()
This is the secret sauce for writing clean, isolated unit and integration tests. You surgically replace the parts that talk to the outside world (databases, auth providers, APIs) with predictable mocks, letting you test your actual application logic in peace.
The DI system is the backbone of a well-structured FastAPI application. It forces you to think about dependencies explicitly, which makes your code more modular, easier to reason about, and a absolute joy to test. Use it everywhere.