Depends() is the single feature that makes FastAPI feel different from Flask or Express. We declare what our handler needs — a DB session, a current user, parsed pagination params — and FastAPI builds those things and injects them. We never call them ourselves.
In simple language — instead of doing user = get_current_user(token) at the top of every handler, we put user: User = Depends(get_current_user) in the signature and FastAPI does it for us, every request.
The core idea
A dependency is just a function. Its parameters can be other request data (Header, Query, Body) or other dependencies. Whatever it returns gets passed into our handler.
Depends(get_db) — calls get_db() first
db parameter
A first example — pagination params
We want ?skip=0&limit=10 on a bunch of list endpoints. Without DI, we copy-paste the same query params everywhere.
from fastapi import FastAPI, Depends
app = FastAPI()
def pagination(skip: int = 0, limit: int = 10) -> dict:
return {"skip": skip, "limit": min(limit, 100)}
@app.get("/users")
def list_users(page: dict = Depends(pagination)):
return {"params": page}
@app.get("/products")
def list_products(page: dict = Depends(pagination)):
return {"params": page}
Both endpoints now accept skip and limit, the cap on limit is in one place, and Swagger picks them up automatically.
A realistic example — current user from JWT
This is the pattern everyone uses for auth.
from fastapi import Header, HTTPException, Depends
def get_current_user(authorization: str = Header(...)) -> dict:
if not authorization.startswith("Bearer "):
raise HTTPException(401, "missing bearer token")
token = authorization.removeprefix("Bearer ")
user = decode_jwt(token) # imagine this exists
if not user:
raise HTTPException(401, "invalid token")
return user
@app.get("/me")
def me(user: dict = Depends(get_current_user)):
return user
@app.post("/posts")
def create_post(payload: PostIn, user: dict = Depends(get_current_user)):
return {"author": user["id"], "title": payload.title}
The Depends(get_current_user) line on every protected endpoint is literally the auth check. If the dependency raises HTTPException, the handler never runs.
The two ways to write it
These are equivalent — pick whichever your team prefers:
# Style 1: in default value
def handler(user: User = Depends(get_current_user)): ...
# Style 2: with Annotated (FastAPI 0.95+, recommended)
from typing import Annotated
CurrentUser = Annotated[User, Depends(get_current_user)]
def handler(user: CurrentUser): ...
The Annotated form lets us alias common dependencies and reuse them. Much cleaner once the codebase grows.
Per-request caching — the magic
If the same dependency appears multiple times in one request (in the handler AND in another dependency), FastAPI calls it once and reuses the result.
Depends(get_db)
called 1x
Depends(get_db)
This is huge — we can use the same get_db dependency in three places and still get one connection per request, not three.
To disable caching for a specific dependency, use Depends(get_db, use_cache=False).
Path / router-level dependencies
When a dependency exists only for its side effects (rate limit check, audit log), we don’t want its return value cluttering the handler signature. Attach it at the route or router level:
def verify_api_key(x_api_key: str = Header(...)):
if x_api_key != "secret":
raise HTTPException(403)
# Single route
@app.get("/admin/stats", dependencies=[Depends(verify_api_key)])
def stats():
return {...}
# Entire router
from fastapi import APIRouter
router = APIRouter(dependencies=[Depends(verify_api_key)])
Why this beats decorators
Frameworks like Flask solve auth with @login_required decorators. The problem — decorators are opaque, can’t be composed cleanly, can’t be type-hinted, and don’t show up in OpenAPI docs.
Depends() is a regular value in the signature. Type checkers see it, IDE autocompletes it, the result is statically typed, and the params it depends on (Header, Query) appear in Swagger UI automatically. We compose dependencies like we compose any other function.
The takeaway — Depends() flips control. Instead of imperatively pulling data inside the handler, we declaratively list what the handler needs and let FastAPI assemble it. Caching, error propagation, and docs all come along for free.