FastAPI’s dependency injection (DI) system is easily its most powerful feature, but it’s also where most developers start making mistakes. When I first started building with FastAPI, I treated Depends() like a convenient way to get a database session and nothing more. As my projects grew, I realized that without a strict set of fastapi dependency injection best practices, my route handlers were becoming bloated, and my tests were becoming a nightmare to maintain.
DI isn’t just about ‘getting a value’; it’s about decoupling your business logic from your infrastructure. Whether you are migrating from a monolith or deciding between FastAPI vs NestJS for enterprise applications, how you handle your dependencies will determine if your project remains maintainable after six months.
1. Favor Class-Based Dependencies for Configuration
Using simple functions for dependencies is great for small projects, but once you have shared configuration (like API keys or environment settings), use a class. This allows you to maintain state or configuration within the dependency itself.
from fastapi import Depends
from pydantic_settings import BaseSettings
class Settings(BaseSettings):
api_key: str
db_url: str
class SettingsDependency:
def __init__(self, settings: Settings = Depends(Settings)):
self.settings = settings
def __call__(self):
return self.settings
# Use it in your route
@app.get("/config")
async def get_config(settings: Settings = Depends(SettingsDependency ())):
return {"api_key": settings.api_key}
2. Implement the Service Layer Pattern
One of the biggest mistakes I see is putting business logic directly inside the route handler. The route should only handle HTTP concerns (parsing request, returning response). Use DI to inject a ‘Service’ class that handles the actual logic.
For those of you working with complex data validation, integrating a Pydantic v2 tutorial for FastAPI approach into your service layer ensures that data is validated before it even hits your business logic.
3. Use Sub-Dependencies for Granular Control
Don’t create one giant get_current_user dependency that does everything. Break it down. Create a dependency for the token, one for the user, and one for the permissions. FastAPI will cache these within a single request, so there is no performance penalty for splitting them.
4. Leverage Dependency Overrides for Testing
This is where the real magic happens. In my experience, the hardest part of backend development is mocking the database. FastAPI allows you to override dependencies globally in your tests.
from fastapi.testclient import TestClient
from main import app, get_db
def override_get_db():
return MockDatabaseSession()
app.dependency_overrides[get_db] = override_get_db
# Now every route using get_db will use the mock
If you’re struggling with this, I highly recommend checking out my comprehensive guide on testing FastAPI backends to see how to structure your pytest suites.
5. Use Annotated for Cleaner Signatures
Since Python 3.9+, using Annotated makes your code significantly more readable and reusable. Instead of repeating Depends(get_db) in ten different routes, define a type alias.
from typing import Annotated
from fastapi import Depends
# Define the type once
DbSession = Annotated[Session, Depends(get_db)]
@app.get("/users")
async def read_users(db: DbSession):
return db.query(User).all()
6. Avoid Global State inside Dependencies
Never use global variables inside a dependency. The beauty of DI is that it makes your code thread-safe and predictable. Always pass the state through the dependency chain.
7. Handle Async vs Sync Dependencies Carefully
If your dependency performs I/O (like a DB call), make it async def. However, if you’re using a synchronous library (like standard SQLAlchemy), define it as a regular def. FastAPI will run regular def dependencies in a separate thread pool to avoid blocking the event loop.
8. Use Dependencies for Cross-Cutting Concerns
Authentication, logging, and rate limiting should always be dependencies. This keeps your main logic clean. If you need to apply a dependency to every route in a router, use the dependencies=[Depends(...)] argument in the APIRouter constructor.
9. Use Yield for Resource Cleanup
When dealing with database connections or file handles, always use yield instead of return. This ensures the connection is closed even if the request crashes.
async def get_db():
db = SessionLocal()
try:
yield db
finally:
db.close()
10. Keep Dependencies Lightweight
Dependencies run on every request. If you have a dependency that performs a heavy computation or a slow API call, consider caching the result or moving it to a background task. A slow dependency is a bottleneck for your entire API.
Common Mistakes to Avoid
- Over-injecting: Injecting 10 different dependencies into one route. If you do this, it’s a sign your route is doing too much; split it into multiple endpoints or a single service class.
- Circular Dependencies: Be careful when Service A depends on Service B, and Service B depends on Service A. This will crash your app on startup.
- Ignoring the Cache: Forgetting that FastAPI caches dependency results within a request, leading to redundant calls when you could have just shared a sub-dependency.
Measuring Success
How do you know if your DI strategy is working? Look for these three signs:
- Test Coverage: You can write unit tests for your services without needing a running database.
- Route Length: Your route handlers are mostly 5-10 lines of code.
- Onboarding Speed: A new developer can understand where the data flows without tracing through five different files.