Skip to content
Module 4 · API & Business LogicIntermediate4 min read · 855 words

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:

python
def get_db():
    db = SessionLocal()
    try:
        yield db
    finally:
        db.close()
python
@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:

  1. Everything before yield (db = SessionLocal()) runs before the endpoint.
  2. The yielded value (db) is injected into the endpoint parameter.
  3. Everything after yield (db.close()) runs after the endpoint returns — inside a finally, 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_db definition. 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_db to 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:

python
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:

python
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 agent

This is a dependency chainget_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.

python
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

  1. Extract get_agent_or_404 as shown above into routers/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.
  2. routers/documents.py defines its own private copies of agent_has_tool() and log_tool_call(), duplicated from routers/agents.py. Turn one of them into a shared dependency or a shared utility module (see Lesson 27 for where a utils.py would live).
  3. Write a fake get_db that yields an in-memory SQLite session, and use app.dependency_overrides to point GET /agents/ at it in a test. What does this buy you that hitting the real agentops.db file wouldn't?

Next Steps

👉 Lesson 11: Authentication →


Module 4 · API & Business Logic