When I first started building with FastAPI, I treated Depends() as just a convenient way to get a database session into my routes. But as my projects grew from simple prototypes to enterprise-grade services, I realized that the difference between a maintainable app and a spaghetti-code nightmare comes down to how you handle your dependencies. Following fastapi dependency injection best practices isn’t just about making the code look clean; it’s about making your application testable and decoupled.
Dependency Injection (DI) in FastAPI is incredibly powerful because it allows you to inject any callable as a dependency. However, with great power comes the tendency to over-engineer. In this post, I’ll share the patterns I’ve found most effective for scaling production backends.
1. Separate Your Logic into Service Layers
One of the biggest mistakes I see is putting business logic directly inside the route handler. Your routes should be thin wrappers that handle HTTP concerns (status codes, request validation) and delegate the actual work to a service class.
# ❌ BAD: Logic in the route
@app.post("/users")
async def create_user(user: UserCreate, db: Session = Depends(get_db)):
user_exists = db.query(User).filter(User.email == user.email).first()
if user_exists:
raise HTTPException(status_code=400, detail="Email registered")
return db.add(User(**user.dict()))
# ✅ GOOD: Service layer injection
class UserService:
def __init__(self, db: Session):
self.db = db
def create_user(self, user_data: UserCreate):
# Business logic lives here
pass
def get_user_service(db: Session = Depends(get_db)) -> UserService:
return UserService(db)
@app.post("/users")
async def create_user(user: UserCreate, service: UserService = Depends(get_user_service)):
return service.create_user(user_data=user)
2. Leverage Pydantic v2 for Dependency Validation
FastAPI relies heavily on Pydantic. With the migration to Pydantic v2, validation has become significantly faster. I recommend using Pydantic models not just for request bodies, but as part of your dependency returns to ensure strict typing across your layers. If you’re still catching up on these changes, check out my Pydantic v2 tutorial for FastAPI to optimize your data models.
3. Use Annotated for Cleaner Signatures
If you have ten routes that all need the current user, repeating user: User = Depends(get_current_user) is tedious and noisy. Since Python 3.9, the Annotated type allows you to define your dependency once and reuse it everywhere.
from typing import Annotated
from fastapi import Depends
# Define the dependency type once
CurrentUser = Annotated[User, Depends(get_current_user)]
@app.get("/profile")
async def read_profile(user: CurrentUser):
return user
@app.get("/settings")
async def read_settings(user: CurrentUser):
return user
4. Avoid Global State in Dependencies
It’s tempting to initialize a database client or an API wrapper globally and just return it in a dependency. However, this makes unit testing a nightmare. Always use a factory function or a class that can be easily swapped during testing. This is a core part of testing FastAPI backends effectively; you want to be able to override the dependency with a mock without restarting the app.
5. Implement Hierarchical Dependencies
FastAPI supports sub-dependencies. You can have a dependency that depends on another dependency. This is great for building a chain of requirements (e.g., get_db → get_user_repo → get_user_service → Route). This creates a clear directed acyclic graph (DAG) of your application’s requirements.
6. Use Dependency Overrides for Integration Testing
The app.dependency_overrides dictionary is your best friend. Instead of hacking your code to accept a test database, simply tell FastAPI to replace the real get_db with a get_test_db during your Pytest setup.
7. Prefer Classes for Complex Dependencies
While functions are great for simple needs, using classes as dependencies allows you to maintain state across the request lifecycle more cleanly. If your dependency requires significant setup or configuration, wrap it in a class with a __call__ method.
8. Be Mindful of Async vs Sync Dependencies
FastAPI is smart about how it runs dependencies. If you define a dependency with async def, it runs in the main event loop. If you use a regular def, FastAPI runs it in a separate threadpool to avoid blocking the loop. Use def for blocking I/O (like traditional SQLAlchemy) and async def for non-blocking I/O (like HTTPX or Motor).
9. Keep Your Dependencies Pure
Dependencies should do one thing: provide a resource or validate a requirement. Avoid putting heavy orchestration logic inside a dependency. If a dependency is doing more than 10-15 lines of logic, it’s probably a service method in disguise.
10. Use Dependencies for Cross-Cutting Concerns
Auth, logging, and rate limiting are perfect candidates for DI. By moving these into dependencies, your route handlers stay focused on the actual business operation. When comparing frameworks, this is where FastAPI often beats others; for example, if you’ve looked at FastAPI vs NestJS for enterprise, you’ll see that while NestJS has a formal DI container, FastAPI’s approach is often more Pythonic and less verbose.
Pro Tip: If you’re struggling with circular imports while setting up your service layers, move your dependency declarations into a separate dependencies.py file.
Common Mistakes
- Over-injecting: Injecting 10 dependencies into one route. This is a sign your route is doing too much. Break it into smaller endpoints or aggregate services.
- Ignoring the Cache: Forgetting that
Depends()caches the result within a single request. If you need a fresh instance every time, be careful with how you structure sub-dependencies. - Hardcoding Config: Injecting a hardcoded string instead of a configuration object. Use a
Settingsclass injected via DI.
Measuring Success
How do you know if your DI strategy is working? Look at your tests. If you can write a full suite of unit tests for your business logic without ever starting a real database or calling a real API, your dependency injection is successful. Your route handlers should be so thin that they barely require testing—the bulk of your tests should target the injected services.