Lesson 10: Dependency Injection
"Don't call us, we'll call you."
What FastAPI's Depends() Actually Does
Depends() tells FastAPI: "before running this endpoint function, call this other function (or generator), and pass its return value as this parameter." It's function injection, not the class-based DI containers you may have used in Java/Spring or .NET.
The Only Dependency This Codebase Actually Has
Grep the whole backend for Depends( and you'll find exactly one dependency function in real use: get_db, defined in database.py:
def get_db():
db = SessionLocal()
try:
yield db
finally:
db.close()@router.get("/{agent_id}", response_model=schemas.AgentResponse)
def get_agent(agent_id: int, db: Session = Depends(get_db)):
...It is used, with db: Session = Depends(get_db), in all 20 endpoints across routers/agents.py and routers/documents.py — this is the single dependency the entire API surface relies on. There is no get_current_user, no get_settings, no custom auth dependency anywhere in the current code — those only exist as illustrative examples further down this lesson and in Lesson 11: Authentication.
Why yield Instead of return?
get_db is a generator function because of the yield. FastAPI treats yield-based dependencies as having setup/teardown phases:
- Everything before
yield(db = SessionLocal()) runs before the endpoint. - The yielded value (
db) is injected into the endpoint parameter. - Everything after
yield(db.close()) runs after the endpoint returns — inside afinally, so the connection is released even if the endpoint raises an exception.
This is the FastAPI equivalent of a context manager (with SessionLocal() as db:), and it's why every request gets its own isolated database session that is guaranteed to close.
What "Testability" and "Reusability" Mean Here, Concretely
The two textbook benefits of DI both apply directly to this code, even in this simple one-dependency codebase:
- Reusability: all 20 endpoints share one
get_dbdefinition. Change how sessions are created (e.g., add connection pooling arguments) in exactly one place, and every endpoint picks it up automatically. - Testability: FastAPI supports dependency overrides — in a test file you can do
app.dependency_overrides[get_db] = get_test_dbto swap in an in-memory SQLite session for every endpoint at once, without touching router code. This codebase doesn't have a test suite yet (Lesson 23: Testing), but this is exactly the mechanism you'd use to build one.
The Missing Dependency: Agent Existence Checking
Here is a real, verifiable pattern from this codebase that is a textbook case for a dependency but isn't implemented as one. Eighteen of the twenty endpoints open with:
agent = db.query(models.Agent).filter(models.Agent.id == agent_id).first()
if agent is None:
raise HTTPException(status_code=404, detail="Agent not found")This exact three-line block is duplicated 18 times across two files. It's a perfect candidate for a dependency:
def get_agent_or_404(agent_id: int, db: Session = Depends(get_db)) -> models.Agent:
agent = db.query(models.Agent).filter(models.Agent.id == agent_id).first()
if agent is None:
raise HTTPException(status_code=404, detail="Agent not found")
return agent
@router.get("/{agent_id}", response_model=schemas.AgentResponse)
def get_agent(agent: models.Agent = Depends(get_agent_or_404)):
return agentThis is a dependency chain — get_agent_or_404 itself depends on get_db. FastAPI resolves the whole chain and, notably, only calls get_db once per request even if multiple dependencies in the chain need it (FastAPI caches dependency results per-request by default). This pattern is not currently used anywhere in the codebase — it's presented here as the fix a code reviewer would ask for. See the exercises below.
Dependencies That Don't Exist Yet (Illustrative Only)
The rest of this section is not implemented in this codebase — it shows the shape custom dependencies would take if/when authentication is added, per Lesson 11.
def get_current_user(token: str = Header(...)):
user = verify_token(token)
if not user:
raise HTTPException(401, "Invalid token")
return user
@router.get("/profile")
def get_profile(user = Depends(get_current_user)):
return {"name": user.name}Key Takeaways
🎯 get_db is the only real dependency in this codebase, and it's used in all 20 endpoints.
🎯 yield-based dependencies give you setup/teardown — db.close() always runs, even on exceptions.
🎯 The repeated "fetch agent or 404" block in 18 endpoints is unfactored duplication, not a hidden dependency — see the exercises.
🎯 FastAPI caches a dependency's result per request, so chaining dependencies doesn't mean redundant work.
Exercises
- Extract
get_agent_or_404as shown above intorouters/agents.py, and refactor three endpoints (get_agent,get_agent_runs,get_agent_tasks) to use it. Confirm the behavior (status codes, response bodies) is identical before and after. routers/documents.pydefines its own private copies ofagent_has_tool()andlog_tool_call(), duplicated fromrouters/agents.py. Turn one of them into a shared dependency or a shared utility module (see Lesson 27 for where autils.pywould live).- Write a fake
get_dbthat yields an in-memory SQLite session, and useapp.dependency_overridesto pointGET /agents/at it in a test. What does this buy you that hitting the realagentops.dbfile wouldn't?
Next Steps
Module 4 · API & Business Logic