If you have spent any time with FastAPI, you have seen Depends() show up in route handler signatures. That single function is the entry point to one of the framework's strongest features: a built-in dependency injection system that handles database sessions, authentication, shared query parameters, and just about any reusable logic you need across your endpoints. This guide explains how the system works, walks through the patterns you will use in practice, and shows how dependency injection makes your FastAPI code dramatically easier to test.
What Dependency Injection Means in FastAPI
Dependency injection is a pattern where a function receives the objects it needs from an external source rather than creating them itself. Instead of your route handler opening a database connection, validating an API key, and parsing query parameters all inline, you declare what you need and let the framework provide it.
In FastAPI, that declaration happens through Depends(). When a request arrives, FastAPI inspects your route handler's signature, identifies every parameter that uses Depends, resolves the full dependency tree, caches results within the request, and injects the resolved values into your function. All of this happens before a single line of your endpoint code runs.
The benefits are practical and immediate. Shared logic (like getting the current user) gets written once and injected everywhere. Database sessions open and close reliably through yield dependencies. Tests swap real services for fakes with a single line of code. And your route handlers stay short because the setup work lives elsewhere.
Function Dependencies: The Building Block
The simplest dependency is a plain function. FastAPI calls it before your endpoint runs and passes the return value as a parameter.
from fastapi import FastAPI, Depends
app = FastAPI()
def common_parameters(q: str | None = None, skip: int = 0, limit: int = 100):
return {"q": q, "skip": skip, "limit": limit}
@app.get("/items/")
def read_items(commons: dict = Depends(common_parameters)):
return commons
@app.get("/users/")
def read_users(commons: dict = Depends(common_parameters)):
return commons
Both /items/ and /users/ now accept q, skip, and limit as query parameters. The dependency bundles them into a dictionary that gets injected into each handler. If you later need to add a new parameter or change the validation logic, you edit one function and every route that depends on it picks up the change.
Notice that common_parameters looks just like a route handler—it takes the same kinds of parameters (query params, headers, etc.) and FastAPI resolves them the same way. The only difference is that you never call the function yourself. FastAPI calls it for you.
Using Annotated for Cleaner Signatures
The recommended modern syntax uses Python's Annotated type to combine the type hint and the dependency in one declaration. This keeps type checkers happy and makes signatures more readable.
from typing import Annotated
from fastapi import Depends, FastAPI
app = FastAPI()
def common_parameters(q: str | None = None, skip: int = 0, limit: int = 100):
return {"q": q, "skip": skip, "limit": limit}
CommonParams = Annotated[dict, Depends(common_parameters)]
@app.get("/items/")
def read_items(commons: CommonParams):
return commons
@app.get("/users/")
def read_users(commons: CommonParams):
return commons
By defining CommonParams as a type alias, you eliminate the Depends() call from every handler signature. This is especially useful when the same dependency appears across many routes.
The Annotated syntax is the style recommended in the official FastAPI documentation. The older = Depends() default-value syntax still works, but Annotated is preferred for new code because it separates the type from the dependency metadata.
Sub-Dependencies: Chaining Logic Together
Dependencies can depend on other dependencies. FastAPI resolves the full tree automatically. This is where dependency injection becomes genuinely powerful—you can build layers of logic without inflating your route signatures.
from typing import Annotated
from fastapi import Depends, FastAPI, Header, HTTPException
app = FastAPI()
def get_api_key(x_api_key: str = Header(...)):
if x_api_key != "secret-key-123":
raise HTTPException(status_code=401, detail="Invalid API key")
return x_api_key
def get_current_user(api_key: Annotated[str, Depends(get_api_key)]):
# In a real app, look up the user associated with this key
return {"user_id": 42, "username": "kandi", "api_key": api_key}
@app.get("/dashboard")
def dashboard(user: Annotated[dict, Depends(get_current_user)]):
return {"message": f"Welcome, {user['username']}"}
When a request hits /dashboard, FastAPI resolves the chain: first get_api_key extracts and validates the header, then get_current_user receives the validated key and looks up the user, and finally the route handler receives the user object. If any step raises an exception, the chain stops and FastAPI returns the error response. The route handler itself has no knowledge of headers or API key validation—it only knows it receives a user.
FastAPI caches dependency results within a single request by default. If two routes in the same request tree share the same dependency, it runs once and the result is reused. To force a fresh call, pass use_cache=False to Depends().
Yield Dependencies: Setup and Cleanup
Many resources need cleanup after use—database sessions should be closed, file handles released, and temporary state cleared. A yield dependency handles both setup and teardown in a single function.
from typing import Annotated
from fastapi import Depends, FastAPI
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker, Session
engine = create_engine("sqlite:///./app.db")
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
def get_db():
db = SessionLocal()
try:
yield db
finally:
db.close()
app = FastAPI()
@app.get("/items/")
def read_items(db: Annotated[Session, Depends(get_db)]):
items = db.execute("SELECT * FROM items").fetchall()
return items
Everything before the yield runs before your endpoint. Everything after the yield runs after the response is sent. The finally block guarantees the session closes even if the endpoint raises an exception. This pattern is the standard way to manage database connections in FastAPI applications.
Do not create the database session inside your route handler and close it manually. If the handler raises an exception before reaching the close call, the session leaks. A yield dependency with a finally block eliminates this risk entirely.
Class Dependencies: When You Need State
For dependencies that benefit from configurable defaults or state, you can use a class. FastAPI treats any callable as a valid dependency, and classes are callable through their __init__ method.
from typing import Annotated
from fastapi import Depends, FastAPI, Query
app = FastAPI()
class Pagination:
def __init__(
self,
page: int = Query(1, ge=1, description="Page number"),
size: int = Query(20, ge=1, le=100, description="Items per page"),
):
self.page = page
self.size = size
self.offset = (page - 1) * size
@app.get("/items/")
def list_items(pagination: Annotated[Pagination, Depends()]):
return {
"page": pagination.page,
"size": pagination.size,
"offset": pagination.offset,
}
When you pass a class to Depends() (or use Depends() without arguments when the type annotation is the class itself), FastAPI instantiates the class using the request parameters. The Pagination class above extracts page and size from query parameters, validates them with constraints, and calculates the offset. Any route that needs pagination adds a single parameter to its signature.
| Pattern | Best For | Example |
|---|---|---|
| Function dependency | Stateless logic, database sessions, auth checks | get_db(), get_current_user() |
| Class dependency | Configurable state, reusable with different defaults | Pagination, FilterParams |
| Yield dependency | Resources that need cleanup (connections, files, locks) | get_db() with yield |
| Sub-dependency chain | Layered logic (auth → user → permissions) | get_api_key → get_current_user |
Router-Level and App-Level Dependencies
You can apply dependencies to every route in a router or even across the entire application. This is useful for cross-cutting concerns like authentication or audit logging that should run on every request.
from fastapi import APIRouter, Depends, FastAPI
def verify_token(x_token: str = Header(...)):
if x_token != "valid-token":
raise HTTPException(status_code=403, detail="Forbidden")
# Every route in this router requires a valid token
admin_router = APIRouter(
prefix="/admin",
tags=["Admin"],
dependencies=[Depends(verify_token)],
)
@admin_router.get("/stats")
def get_stats():
return {"users": 1024, "active": 812}
@admin_router.get("/logs")
def get_logs():
return {"entries": 47}
app = FastAPI()
app.include_router(admin_router)
The dependencies parameter on APIRouter runs verify_token before every route in that router. The individual route handlers do not need to declare the dependency themselves—it happens automatically. This prevents the common mistake of adding a new admin route and forgetting to include the auth check.
You can also apply dependencies at the application level by passing them to the FastAPI constructor: app = FastAPI(dependencies=[Depends(verify_token)]). This runs the dependency on every route in the entire application.
Testing With dependency_overrides
One of the strongest practical benefits of dependency injection is testability. FastAPI provides a dependency_overrides dictionary that lets you swap any dependency with a replacement during tests—no patching or monkey-patching required.
# test_items.py
from fastapi.testclient import TestClient
from app.main import app, get_db
# Create a test database session
def get_test_db():
test_db = TestSessionLocal()
try:
yield test_db
finally:
test_db.close()
# Override the real dependency with the test version
app.dependency_overrides[get_db] = get_test_db
client = TestClient(app)
def test_read_items():
response = client.get("/items/")
assert response.status_code == 200
def test_create_item():
response = client.post("/items/", json={"title": "Test Item"})
assert response.status_code == 201
# Clean up overrides after tests
app.dependency_overrides.clear()
The key line is app.dependency_overrides[get_db] = get_test_db. This tells FastAPI: wherever get_db would normally be called, use get_test_db instead. The route handlers, the sub-dependencies, and the rest of the application code remain completely unchanged. You can override authentication dependencies to bypass login during tests, swap external API clients with mock versions, or point database sessions at an in-memory SQLite instance.
Always call app.dependency_overrides.clear() after your tests to prevent overrides from leaking into other test modules. In pytest, a fixture with yield is a clean way to handle this automatically.
Frequently Asked Questions
What is dependency injection in FastAPI?
Dependency injection in FastAPI is a system where your route handlers declare what they need—database sessions, authenticated users, configuration values—using the Depends() function. FastAPI resolves and provides those objects automatically when a request arrives. This keeps endpoint code focused on business logic and makes dependencies swappable for testing.
What is the difference between a function dependency and a class dependency in FastAPI?
A function dependency is a regular Python function that FastAPI calls before your endpoint runs. A class dependency uses a class whose __init__ method accepts the same kinds of parameters. Functions are simpler and work for stateless logic like database sessions and auth checks. Classes are useful when you need configurable state, such as a pagination helper with adjustable default values for page size.
How do I test FastAPI endpoints that use dependencies?
Use the dependency_overrides dictionary on your FastAPI app instance. Assign a replacement function as the value and the original dependency as the key. FastAPI will use the override everywhere during tests. This lets you swap database connections for in-memory alternatives, bypass authentication, or replace external services with fakes—all without changing application code.
Are dependencies called on every request?
Yes, dependencies run fresh on each incoming request. However, within a single request, FastAPI caches the result of each dependency by default. If multiple parts of the dependency tree require the same dependency, it executes once and the cached result is reused. You can disable this caching by passing use_cache=False to Depends().
Key Takeaways
- Depends() is the core mechanism: Every dependency—function, class, or generator—is declared through
Depends(). FastAPI resolves the dependency tree, caches results per-request, and injects values into your route handler before it runs. - Use yield for resources that need cleanup: Database sessions, file handles, and temporary connections should use
yielddependencies with afinallyblock. This guarantees cleanup happens even when exceptions occur. - Sub-dependencies build layered logic: Dependencies can depend on other dependencies. Build authentication chains (API key → user → permissions) without putting any of that logic in your route handlers.
- Router-level dependencies prevent mistakes: Apply authentication and other cross-cutting concerns at the router level so that new routes inherit the checks automatically.
- dependency_overrides makes testing painless: Swap real dependencies for test doubles with a single dictionary assignment. No patching, no monkey-patching, no changes to application code.
FastAPI's dependency injection system is one of those features that looks simple in a tutorial but transforms how you structure a real application. Once you start treating database sessions, authentication, pagination, and shared query parameters as injectable dependencies, your route handlers shrink to a few lines of focused business logic. Your tests become straightforward because every external concern can be swapped out. And your codebase stays modular because dependencies enforce clean boundaries between layers.