FastAPI dependencies declare what a route needs without repeating authentication, pagination, or session setup. Depends usually receives a function or callable and supplies its result to the endpoint.
Review the FastAPI REST guide first if route operations are new to you.
Reuse typed parameters
from typing import Annotated
from fastapi import Depends, FastAPI, Query
app = FastAPI()
def pagination(page: Annotated[int, Query(ge=1)] = 1, limit: int = 20):
return page, min(limit, 100)
Pagination = Annotated[tuple[int, int], Depends(pagination)]
@app.get("/products")
def list_products(values: Pagination):
page, limit = values
return {"page": page, "limit": limit}
The official dependencies tutorial recommends the Annotated style in current versions.
Dependencies can depend on others. An authentication dependency may read a header, while current_user validates the token. FastAPI resolves the graph and caches repeated results for that request. The sub-dependencies guide explains use_cache=False.
Manage resources with yield
def get_session():
session = SessionLocal()
try:
yield session
finally:
session.close()
Make commit and rollback boundaries explicit. The SQLAlchemy guide covers the data layer.
In tests, replace external resources:
app.dependency_overrides[get_session] = fake_session
try:
assert client.get("/products").status_code == 200
finally:
app.dependency_overrides.clear()
Overrides improve isolation but do not eliminate integration tests. Small dependencies with clear contracts are easier to reuse, replace, and reason about.
Understand what FastAPI injects
FastAPI inspects the dependency callable just as it inspects a path operation. Parameters can come from the query string, headers, cookies, path, body, or other dependencies. The returned value is passed to the parameter that declared Depends. This is runtime dependency resolution, not automatic construction of every class in the application.
Use a type alias when many routes share the same declaration:
from typing import Annotated
Page = Annotated[tuple[int, int], Depends(pagination)]
@app.get("/orders")
def list_orders(page: Page):
number, size = page
return find_orders(number=number, size=size)
The annotation continues to tell editors and type checkers what the endpoint receives. Avoid hiding domain logic inside aliases with vague names. A reader should be able to find which validation runs.
Build an authentication dependency chain
Authentication works well as a set of focused dependencies: extract credentials, validate them, then enforce a specific permission.
from fastapi import Header, HTTPException, status
def bearer_token(
authorization: Annotated[str | None, Header()] = None,
) -> str:
prefix = "Bearer "
if authorization is None or not authorization.startswith(prefix):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Missing bearer token",
headers={"WWW-Authenticate": "Bearer"},
)
return authorization.removeprefix(prefix)
def current_user(token: Annotated[str, Depends(bearer_token)]) -> User:
user = token_service.verify(token)
if user is None:
raise HTTPException(status_code=401, detail="Invalid token")
return user
def active_admin(user: Annotated[User, Depends(current_user)]) -> User:
if not user.active or "admin" not in user.roles:
raise HTTPException(status_code=403, detail="Insufficient permission")
return user
A missing or invalid credential usually produces 401; an authenticated user lacking permission produces 403. Do not accept the token from an arbitrary query parameter because URLs are commonly logged. Real token verification must check signature, allowed algorithm, expiration, issuer, and audience as appropriate. Dependency injection organizes those checks but does not make an insecure verifier safe.
Use callable classes for configurable behavior
Any callable object can be a dependency. A class instance is useful when the rule needs configuration:
class RequireRole:
def __init__(self, role: str):
self.role = role
def __call__(self, user: Annotated[User, Depends(current_user)]) -> User:
if self.role not in user.roles:
raise HTTPException(status_code=403, detail="Insufficient permission")
return user
require_editor = RequireRole("editor")
@app.post("/articles")
def create_article(user: Annotated[User, Depends(require_editor)]):
return {"author_id": user.id}
Create configuration objects at startup rather than rebuilding them in each request. Keep request-specific mutable state out of shared instances unless access is synchronized correctly.
Treat yield dependencies as context managers
A dependency that yields has an acquisition phase and a cleanup phase. Cleanup runs after FastAPI finishes using the dependency, including when an exception occurs:
def get_session():
with SessionLocal() as session:
try:
yield session
session.commit()
except Exception:
session.rollback()
raise
Whether commit belongs here is an architectural choice. Automatic commit can be convenient, but it can also hide transaction boundaries and commit after a route performed several unrelated operations. Many teams keep get_session responsible only for opening and closing, then commit explicitly in a service. Whichever convention you choose, document it and test rollback.
Dependencies can be synchronous or asynchronous. Use async def when awaiting an asynchronous driver or client. Do not merely change CPU-bound or blocking database work to async def; blocking work still blocks the event loop. Match the dependency to the libraries it calls.
The official yield dependency guide explains execution and cleanup details.
Control caching per request
When the same dependency appears more than once in one dependency graph, FastAPI normally calls it once and reuses its value for that request. This is useful for current_user and database sessions. It is not a global application cache and does not persist between requests.
Set use_cache=False only when every declaration truly needs a new result:
def request_nonce() -> str:
return secrets.token_urlsafe(16)
Nonce = Annotated[str, Depends(request_nonce, use_cache=False)]
Repeated queries, token verification, and session creation are usually better cached within the request. For cross-request caching, use an explicit cache with an expiration and invalidation policy.
Apply dependencies without receiving a value
Sometimes a dependency only validates or records something. Declare it on the route or router:
def require_internal_key(x_api_key: Annotated[str, Header()]) -> None:
if not secrets.compare_digest(x_api_key, settings.internal_api_key):
raise HTTPException(status_code=401, detail="Invalid API key")
@app.get("/internal/health", dependencies=[Depends(require_internal_key)])
def internal_health():
return {"status": "ok"}
Router-level dependencies are useful for a group of routes. Application-wide dependencies affect every operation, including documentation or health endpoints if they are in scope, so apply them carefully. Side effects such as audit logging should be explicit and resilient; a failed analytics service should not necessarily make the main request fail.
Override dependencies safely in tests
Overrides are keyed by the original callable object, not its name. Replacing get_session with another generator lets tests control cleanup:
def override_session():
session = TestSession()
try:
yield session
finally:
session.close()
app.dependency_overrides[get_session] = override_session
try:
response = client.get("/orders")
assert response.status_code == 200
finally:
app.dependency_overrides.clear()
Clearing overrides is essential because the application object often lives across tests. A pytest fixture can install the mapping and clear it in teardown. Override external authentication and databases in focused route tests, but keep integration tests that exercise the production dependency wiring, database transaction behavior, and real token validation.
Avoid a service locator
Dependencies should expose a small, typed contract. A dependency that returns a huge container and lets routes fetch arbitrary services hides coupling and weakens type checking. Likewise, putting every business rule inside dependencies makes endpoints difficult to follow. Dependencies are best for request-scoped coordination: credentials, configuration, sessions, shared parameters, and lightweight service construction. Domain decisions belong in services or domain objects that can also be tested without FastAPI.
Watch for dependencies that perform repeated network calls, mutate global state, catch broad exceptions, or return different unrelated types. These are signals to split responsibilities. FastAPI displays dependency parameters in OpenAPI automatically, so verify that headers and queries exposed by dependencies are documented correctly and that secrets never appear as example values.
Debug the dependency graph
When a route returns 422, identify which dependency parameter FastAPI could not validate. A value accidentally declared as a query parameter instead of a header prevents the endpoint from running. Use explicit annotations and inspect its location and constraints in OpenAPI.
For repeated work, trace callable identities. Two wrappers with identical logic remain different dependencies and do not share a cached result. For cross-user data, remember that the dependency cache lasts one request; investigate global objects, application caches, or shared mutable services.
Test missing input, malformed credentials, inactive users, insufficient permission, cleanup after exceptions, and independent concurrent requests. Route tests can override slow services, while integration tests should exercise real wiring, transaction behavior, and token validation. Apply common rules at router level carefully: a dependency listed only in dependencies=[...] discards its result, while a route that needs the user should receive a typed parameter. Review the generated API documentation as a client would; headers, error codes, and models must match actual behavior.