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()
Comparison of standard Depends() syntax versus Annotated type aliasing in FastAPI
Comparison of standard Depends() syntax versus Annotated type aliasing in FastAPI

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

Measuring Success

How do you know if your DI strategy is working? Look for these three signs:

  1. Test Coverage: You can write unit tests for your services without needing a running database.
  2. Route Length: Your route handlers are mostly 5-10 lines of code.
  3. Onboarding Speed: A new developer can understand where the data flows without tracing through five different files.