Dependency injection with Depends()

The problem: repeated logic across routes

Several routes often need the exact same piece of setup or validation — pagination parameters, a database connection, a shared check — and writing it out in every route function duplicates it:

from fastapi import Query

@app.get("/agents")
def list_agents(skip: int = Query(default=0, ge=0), limit: int = Query(default=10, gt=0, le=100)):
    return {"skip": skip, "limit": limit}

@app.get("/tools")
def list_tools(skip: int = Query(default=0, ge=0), limit: int = Query(default=10, gt=0, le=100)):
    return {"skip": skip, "limit": limit}

Identical skip/limit definitions, copied into every route that needs pagination — a maintenance problem the moment the constraints need to change (raising the max limit, say) in more than one place.

Depends() — declaring a shared dependency once

A plain function, used as a parameter’s default via Depends(...), lets FastAPI call that function for you and inject its return value — written once, reused everywhere:

from fastapi import Depends, Query

def pagination_params(skip: int = Query(default=0, ge=0), limit: int = Query(default=10, gt=0, le=100)):
    return {"skip": skip, "limit": limit}

@app.get("/agents")
def list_agents(pagination: dict = Depends(pagination_params)):
    return pagination

@app.get("/tools")
def list_tools(pagination: dict = Depends(pagination_params)):
    return pagination
The same dependency, backing two different routes
click Run to see this pane's output

Depends(pagination_params) tells FastAPI: before running list_agents, call pagination_params(...) (itself receiving skip/limit from the query string, exactly as before), and pass its return value in as pagination. Both routes now share one single definition — changing the limit constraint in pagination_params updates every route that depends on it, automatically.

A dependency that returns a real, meaningful object

Pagination returning a plain dict is a simple case — dependencies commonly stand in for something more substantial, like a shared resource:

def get_agent_registry():
    return agents_db   # a shared dict, database session, or similar resource

@app.get("/agents/{agent_id}")
def get_agent(agent_id: int, registry: dict = Depends(get_agent_registry)):
    return registry.get(agent_id, {})
The dependency's return value, used directly
click Run to see this pane's output

get_agent_registry here is trivial, but the pattern generalizes directly to something like a real database session — a dependency function that opens a connection, and every route needing database access declares Depends(get_db_session) rather than opening its own connection by hand.

A dependency used only for its side effect

A dependency doesn’t need its return value to matter — sometimes the point is purely the validation or side effect it performs, raising an HTTPException before the route even runs if some shared condition fails:

from fastapi import Header, HTTPException

def verify_request_id(x_request_id: str = Header(default=None)):
    if x_request_id is None:
        raise HTTPException(status_code=400, detail="X-Request-Id header is required")

@app.get("/agents", dependencies=[Depends(verify_request_id)])
def list_agents():
    return {"agents": []}
Missing header vs. present header
click Run to see this pane's output

dependencies=[Depends(verify_request_id)] on the route decorator itself (rather than as a function parameter) runs verify_request_id before list_agents, without needing its result injected anywhere — its only job is to reject the request early if the header’s missing, which it does exactly like any other raise you’ve written.

Check your understanding
1/4

What problem does Depends() solve, compared to repeating the same parameter definitions across multiple routes?