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_dbget_user_repoget_user_serviceRoute). This creates a clear directed acyclic graph (DAG) of your application’s requirements.

Technical diagram showing the flow of FastAPI dependencies from DB to Route
Technical diagram showing the flow of FastAPI dependencies from DB to Route

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

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.