Skip to content
ExponaBLOG
PYTHON BACKEND DEVELOPMENT · DAY 017 / 100

Dependency injection in FastAPI: lessons from production

Depends() for database sessions, auth and shared logic.

Expona Admin6 min read

Dependency injection in FastAPI: lessons from production

When your FastAPI application grows beyond a handful of routes, managing shared resources like database connections, authentication tokens, and business logic becomes fragile and error-prone. Dependency injection is the pattern that transforms this chaos into a maintainable system, and FastAPI's Depends() mechanism makes it straightforward to implement at scale.

In this article, we'll walk through how we've structured dependency injection across production systems handling ride-hailing, e-commerce, and SaaS workloads. You'll learn to build composable, testable, and efficient dependency chains that keep your codebase clean and your resources properly managed.

Why dependency injection matters in production

Without structured dependency injection, your handlers end up creating resources inline: opening database connections, parsing tokens, querying user tables. This approach couples your business logic to infrastructure, makes testing painful, and leads to resource leaks when exceptions occur.

FastAPI's Depends() function solves this by making dependencies explicit and reusable. The framework handles the dependency graph, scope management, and cleanup automatically. When you declare a dependency, FastAPI ensures it's instantiated once per request (by default) and made available wherever you need it.

Database session dependency: open per request, close afterward

The most common dependency in any data-driven application is the database session. In production systems, we always follow the pattern of opening a session at the start of the request, using it throughout the handler, and closing it at the end—even if an error occurs.

Here's the foundation we use across our teams:

python
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker, Session

DATABASE_URL = "postgresql://user:password@localhost/mydb"
engine = create_engine(DATABASE_URL)
SessionLocal = sessionmaker(bind=engine)

def get_db():
    db = SessionLocal()
    try:
        yield db
    finally:
        db.close()

The yield pattern is crucial here. FastAPI recognizes generator functions as cleanup handlers: the code before yield runs before the route handler, and the code after runs afterward. This ensures your session closes even if your route raises an exception.

We inject this dependency into handlers like this:

python
from fastapi import FastAPI, Depends
from sqlalchemy.orm import Session

app = FastAPI()

@app.get("/users/{user_id}")
def get_user(user_id: int, db: Session = Depends(get_db)):
    user = db.query(User).filter(User.id == user_id).first()
    return user

FastAPI parses the type annotation Session and the Depends(get_db) call, invokes the dependency function, and passes its result to your handler. If you later need to swap SessionLocal for a different database, you change only the get_db() function—your route handlers remain untouched.

Injecting dependencies into route handlers

Once you've defined a dependency, injection is declarative and composable. Your route handlers list their dependencies as function parameters with type hints and Depends() calls.

Here's a more realistic example from an e-commerce system:

python
@app.post("/orders")
def create_order(
    user_id: int,
    order: OrderSchema,
    db: Session = Depends(get_db),
    auth_token: str = Depends(get_auth_token),
):
    # Verify token is valid
    user = verify_token(auth_token)
    
    # Create order in database
    new_order = Order(user_id=user.id, items=order.items)
    db.add(new_order)
    db.commit()
    
    return {"id": new_order.id, "status": "created"}

Notice we're injecting both the database session and an authentication token. FastAPI resolves both before calling the handler, and cleans up both afterward. This keeps your handler focused on business logic rather than resource management.

Chaining dependencies: composing complex workflows

Real production systems rarely have flat dependency trees. You often need one dependency to depend on another. For instance, extracting the current user requires both a database session and a token from the request header.

Here's how we structure this in practice:

python
from fastapi import Header
from fastapi.exceptions import HTTPException

def get_auth_token(authorization: str = Header(None)) -> str:
    if not authorization:
        raise HTTPException(status_code=401, detail="Missing auth token")
    # Strip "Bearer " prefix
    return authorization.replace("Bearer ", "")

def get_current_user(
    token: str = Depends(get_auth_token),
    db: Session = Depends(get_db),
) -> User:
    # Validate token and fetch user from database
    payload = decode_jwt(token)
    user = db.query(User).filter(User.id == payload.get("user_id")).first()
    if not user:
        raise HTTPException(status_code=401, detail="User not found")
    return user

Now any route handler can depend on get_current_user, and FastAPI automatically resolves the full chain: extract the token from headers, open a database session, validate the token, fetch the user, and pass the user object to your handler.

python
@app.get("/profile")
def get_profile(current_user: User = Depends(get_current_user)):
    return {"id": current_user.id, "email": current_user.email}

FastAPI is smart about caching. If multiple dependencies in a single request depend on get_db, FastAPI invokes it only once and reuses the same session throughout the request. This avoids unnecessary database connections and ensures all queries within a request see consistent data.

Testing with dependency overrides

The real power of dependency injection shows up in testing. Your test suite can replace real dependencies with fakes or mocks using app.dependency_overrides.

Consider a CRM system where you want to test order creation without hitting the real database:

python
from fastapi.testclient import TestClient
from unittest.mock import Mock

client = TestClient(app)

def test_create_order():
    # Create a mock database session
    mock_db = Mock()
    mock_db.add = Mock()
    mock_db.commit = Mock()
    
    # Create a fake user
    fake_user = User(id=1, email="test@example.com")
    
    # Override dependencies
    app.dependency_overrides[get_db] = lambda: mock_db
    app.dependency_overrides[get_current_user] = lambda: fake_user
    
    try:
        response = client.post(
            "/orders",
            json={"items": [{"product_id": 1, "quantity": 2}]}
        )
        assert response.status_code == 200
        assert response.json()["status"] == "created"
    finally:
        # Always clear overrides after tests
        app.dependency_overrides.clear()

Notice the finally block—it's essential to clear overrides after each test to avoid state leaking between tests. We typically do this in a pytest fixture:

python
import pytest

@pytest.fixture(autouse=True)
def reset_overrides():
    yield
    app.dependency_overrides.clear()

With this pattern, your tests run fast, don't require a real database, and can inject specific scenarios (invalid tokens, missing users, database errors) without modifying your handler code.

Common mistakes

  • Forgetting to close resources. Always use try/finally or context managers in your dependency functions. A single unclosed database connection per request adds up quickly in production.
  • Not clearing dependency overrides between tests. Leaving overrides active causes test pollution and flaky test suites. Use a pytest fixture with autouse=True to reset after each test.
  • Overusing dependencies for simple parameters. Not every parameter needs to be a dependency. Use Depends() when you need resource management, cleanup, or reuse across handlers. Simple path or query parameters don't need it.
  • Creating circular dependencies. If dependency A depends on B and B depends on A, FastAPI will hang or raise an error. Design your dependency tree to be acyclic; if you find yourself in a circle, refactor shared logic into a separate service layer.
  • Mixing business logic into dependencies. Dependencies should manage resources and cross-cutting concerns. Complex domain logic belongs in service classes or functions that your dependencies invoke.

Key takeaways

  • Use yield in dependency functions to guarantee cleanup even when exceptions occur. This pattern keeps your database connections and other resources properly managed.
  • Chain dependencies to compose complex workflows. FastAPI resolves the full graph before invoking your handler, and caches results across a single request.
  • Override dependencies in tests to replace real services with mocks. This keeps tests fast, isolated, and focused on your business logic rather than infrastructure.
  • Design your dependency tree to be acyclic and keep dependencies focused on resource management. Push complex business logic into service layers that dependencies call, not into the dependencies themselves.
  • Dependency injection transforms FastAPI applications from fragile, tightly coupled systems into maintainable, testable codebases that scale with your team.
ShareXLinkedInReddit

Found this useful? Support the writing.

Buy me a coffee
Written by
Expona Admin
Site administrator

Comments

Loading comments…

Leave a comment

Comments are reviewed before they appear. Be kind and stay on topic.

Related articles