Last modified: Dec 02, 2025 By Alexander Williams
FastAPI Dependency Injection Best Practices
FastAPI dependency injection is a powerful feature. It helps you write clean code. It makes your application easier to test. It also improves maintainability.
This guide covers the best practices. You will learn key patterns. You will see practical examples. Let's build better APIs.
What is Dependency Injection?
Dependency Injection (DI) is a design pattern. It provides objects a dependency. The dependency is from outside. This is called inversion of control.
In FastAPI, you use the Depends function. It declares dependencies for path operations. This keeps your code modular.
Your functions become simpler. They focus on their main logic. Dependencies handle setup and teardown.
Core Benefits of Using Dependencies
Dependencies offer many advantages. They reduce code duplication. You can reuse logic across endpoints.
They simplify testing. You can easily mock dependencies. Your business logic stays isolated.
Dependencies improve security. You can create reusable authentication checks. This is essential for secure applications.
Basic Dependency Pattern
Start with a simple dependency. A dependency is just a callable. It can be a function or a class.
from fastapi import Depends, FastAPI
app = FastAPI()
# A simple dependency function
def common_parameters(q: str = None, skip: int = 0, limit: int = 100):
return {"q": q, "skip": skip, "limit": limit}
@app.get("/items/")
async def read_items(commons: dict = Depends(common_parameters)):
# The 'commons' dict is injected by FastAPI
return commons
The common_parameters function is a dependency. It extracts query parameters. The read_items endpoint uses it.
FastAPI calls the dependency. It passes the result to your path operation function. This is the core pattern.
Best Practice 1: Use Classes for Complex Dependencies
Use classes for stateful or complex logic. A class can have an __init__ method. It can also have other methods.
from fastapi import Depends
class DatabaseSession:
def __init__(self):
# Simulate a database connection
self.session = "active_session"
print("Database session created")
def get_data(self):
return {"data": "from database"}
def close(self):
print("Database session closed")
self.session = None
# A dependency that yields the session
def get_db():
db = DatabaseSession()
try:
yield db
finally:
db.close()
@app.get("/data/")
async def fetch_data(db: DatabaseSession = Depends(get_db)):
result = db.get_data()
return result
This pattern is great for resources. Use it for database connections. Use it for external API clients.
The yield keyword is key. It provides the object. Cleanup runs after the response is sent.
Best Practice 2: Layer Your Dependencies
Dependencies can depend on other dependencies. This creates a dependency tree. It promotes code reuse.
from fastapi import Header, HTTPException, Depends
def verify_token(authorization: str = Header(...)):
if authorization != "Bearer secret-token":
raise HTTPException(status_code=401, detail="Invalid token")
return {"user_id": "123"}
def get_current_user(token_info: dict = Depends(verify_token)):
# You could fetch a user from a database here
user = {"id": token_info["user_id"], "name": "John Doe"}
return user
@app.get("/profile/")
async def user_profile(current_user: dict = Depends(get_current_user)):
return {"message": f"Hello, {current_user['name']}"}
get_current_user depends on verify_token. This separates concerns. Authentication is distinct from user fetching.
This is similar to patterns used in FastAPI JWT Authentication Python Tutorial.
Best Practice 3: Keep Dependencies Pure and Testable
Write dependencies as pure functions when possible. Avoid side effects in simple dependencies. This makes them easy to test.
Pass configuration and clients as arguments. Do not hardcode them inside the dependency.
# Good: Configurable and testable
def get_paginated_params(skip: int = 0, limit: int = 100):
return {"skip": skip, "limit": limit}
# Avoid: Hardcoded values inside
def bad_pagination():
# Hard to test with different limits
return {"skip": 0, "limit": 50}
Pure dependencies are predictable. You can unit test them in isolation. Mocking is straightforward.
Pattern: Resource Provider with Yield
This is a critical pattern for resources. It ensures proper cleanup. Use it with databases, file handles, or network connections.
# Simulating an external service client
class ExternalAPIClient:
def connect(self):
print("Connected to external API")
return self
def fetch(self):
return "External data"
def disconnect(self):
print("Disconnected from external API")
def get_external_client():
client = ExternalAPIClient().connect()
try:
yield client
finally:
client.disconnect()
@app.get("/external/")
async def get_external_data(client: ExternalAPIClient = Depends(get_external_client)):
data = client.fetch()
return {"data": data}
# Expected console output when calling the endpoint:
Connected to external API
Disconnected from external API
The cleanup in the finally block always runs. This is true even if an error occurs in your endpoint.
Pattern: Parameterized Dependencies
Sometimes you need to customize a dependency. You can create a function that returns a dependency. This is a factory pattern.
from fastapi import Query
def role_required(required_role: str):
# This is a dependency factory
def check_user_role(current_user: dict = Depends(get_current_user)):
if current_user.get("role") != required_role:
raise HTTPException(status_code=403, detail="Insufficient permissions")
return current_user
return check_user_role
@app.get("/admin/")
async def admin_dashboard(user=Depends(role_required("admin"))):
return {"message": "Welcome, admin"}
The role_required function is a factory. It creates a new dependency for each required role. This is very flexible.
Common Pitfalls to Avoid
Do not overuse dependencies. Simple logic can stay in the path function. Dependencies are for shared, complex logic.
Avoid making dependencies too large. A dependency should have a single responsibility. This aligns with the Single Responsibility Principle.
Remember that dependencies are evaluated for each request. Do not put heavy initialization inside unless necessary. For heavy tasks, consider FastAPI Background Tasks.
Testing Dependencies
Testing is simple with FastAPI's Depends system. You can test dependencies in isolation. You can also test endpoints with mocked dependencies.
Use the TestClient from FastAPI. Override dependencies in your app for testing. Learn more in our guide on Test FastAPI Endpoints with pytest and TestClient.
from fastapi.testclient import TestClient
# Example test for a dependency
def test_common_parameters():
result = common_parameters(q="test", skip=10, limit=50)
assert result == {"q": "test", "skip": 10, "limit": 50}
Conclusion
FastAPI dependency injection is a cornerstone feature. It leads to cleaner, more maintainable code.
Use the patterns shown here. Start with simple functions. Move to classes and yield for resources. Layer dependencies for complex logic.
Keep dependencies pure and testable. Avoid common pitfalls. Your APIs will be robust and scalable.
Combine this with other FastAPI features. Use Pydantic models for validation. Structure your data access with SQLAlchemy. Build comprehensive applications.