FastAPI Dependencies: Shared Database Sessions and Auth Without the Boilerplate
FastAPI's Depends system eliminates duplicated session and auth code across endpoints. A worked example shows yield-based cleanup, per-request caching, and how to verify both behaviors yourself.
09 Jun 2026, 16:07 UTC

Every FastAPI tutorial starts the same way: you write a path operation, it works, and then you write a second one that needs the same database session, the same "who is this user?" check, the same pagination parsing. By the fifth endpoint you have copy-pasted setup code everywhere, and changing how a session is created means editing a dozen functions. FastAPI's dependency injection system exists precisely for this problem, and the useful takeaway is simple: anything your endpoints share — sessions, auth, config — should be a Depends dependency, written once and resolved per request.
What Depends actually does
Depends tells FastAPI: "before running this path operation, call this other callable and hand me its return value." The callable can be a plain function, an async function, a class, or a generator. FastAPI inspects its type hints, so a dependency that takes a Header or Query parameter gets validation and OpenAPI documentation for free — the same as if you had declared it on the endpoint itself.
Two properties make this more than a convenience wrapper:
- Hierarchical resolution. A dependency can itself declare dependencies. FastAPI builds a small graph per request and resolves it in order.
- Per-request caching. If two dependencies in the same request both need the same sub-dependency, it runs once and the result is reused. Your "get current user" function will not hit the database twice just because both the endpoint and a permission check depend on it.
A worked example: session plus current user
Here is the canonical pattern — a generator dependency that yields a database session and guarantees cleanup, plus an auth dependency layered on top. This assumes FastAPI 0.95+ (where Annotated is the recommended style) and SQLAlchemy 2.x:
from typing import Annotated
from fastapi import Depends, FastAPI, Header, HTTPException
from sqlalchemy import create_engine
from sqlalchemy.orm import Session, sessionmaker
engine = create_engine("postgresql+psycopg://app:secret@db:5432/appdb")
SessionLocal = sessionmaker(bind=engine, autoflush=False)
def get_db():
db = SessionLocal()
try:
yield db
finally:
db.close()
def get_current_user(
authorization: Annotated[str, Header()],
db: Annotated[Session, Depends(get_db)],
):
token = authorization.removeprefix("Bearer ").strip()
user = lookup_user_by_token(db, token) # your auth logic
if user is None:
raise HTTPException(status_code=401, detail="Invalid token")
return user
app = FastAPI()
@app.get("/me")
def read_me(user: Annotated[dict, Depends(get_current_user)]):
return {"user_id": user["id"], "email": user["email"]}Run this with uvicorn main:app --reload from the directory containing the file. No special permissions are needed beyond network access to your database. Replace lookup_user_by_token and the connection string with your real values.
The flow per request: FastAPI calls get_db, passes the session into get_current_user, which validates the Authorization header and either raises a 401 or returns the user. When the response finishes — success or exception — code after the yield runs and the session closes. You never write db.close() in an endpoint again.
Verifying the behavior you were promised
Don't take the caching and cleanup claims on faith; both are easy to check:
- Cleanup: add a
print("closing session")after theyieldinget_db, hit the endpoint, and confirm the message appears after the response is returned. If it never prints, something is swallowing the generator teardown. - Caching: make a second dependency that also does
Depends(get_db), add a counter or log line at the top ofget_db, and confirm it executes once per request, not twice.
A quick smoke test with curl -H "Authorization: Bearer test" http://localhost:8000/me should return either your user JSON or the 401 detail — both prove the dependency chain ran.
Trade-offs worth knowing before you go all-in
Blocking I/O in sync dependencies. A regular def dependency runs in a thread pool, which is fine for SQLAlchemy-style sync drivers. But if you mix a slow sync dependency into an otherwise async app under load, you can exhaust that pool. If your driver supports async (e.g., asyncpg, AsyncSession), prefer async def dependencies.
Deep nesting hurts debuggability. Dependencies depending on dependencies depending on dependencies produce stack traces where the actual failure is four frames removed from your endpoint. Keep chains shallow — two or three levels covers session → user → permission comfortably.
Hidden parameters. Because dependencies declare their own headers and queries, it is easy to lose track of what an endpoint actually requires. The Swagger UI at /docs shows the merged picture; check it after wiring up a new dependency to confirm the API surface looks like you intended.
Where to start
Pick the one piece of logic you have duplicated across the most endpoints — usually the database session — and convert it to a yield-based dependency today. Then layer auth on top of it. Once you see the per-request caching and guaranteed cleanup working in your logs, the rest of your shared logic will follow naturally.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.