FastAPI: Dependency Injection and Reusable Service Boundaries

Introduction

As a FastAPI application grows, route handlers can easily become responsible for too many things: validating requests, querying databases, applying business rules, and formatting responses. This makes endpoints difficult to test and encourages duplicated logic.

FastAPI’s dependency injection system provides a convenient way to share resources and define reusable application boundaries. Combined with a clear service layer, it helps keep route handlers focused on HTTP concerns.

1. Start with a simple dependency

FastAPI uses Depends to declare that a function needs another function’s result.

from typing import Annotatedfrom fastapi import Depends, FastAPIapp = FastAPI()def get_current_version() -> str:    return "1.0"Version = Annotated[str, Depends(get_current_version)]@app.get("/version")def read_version(version: Version):    return {"version": version}

FastAPI resolves the dependency before invoking the endpoint. Dependencies can themselves declare dependencies, allowing an application to compose authentication, configuration, database access, and other reusable behavior.

For reusable annotations, Annotated can make endpoint signatures easier to read.

2. Keep business logic outside route handlers

Imagine an endpoint that retrieves a customer. Instead of putting database operations and business rules directly into the route, delegate them to a service.

class CustomerService:    def __init__(self, repository):        self.repository = repository    def get_customer(self, customer_id: int):        customer = self.repository.get_by_id(customer_id)        if customer is None:            return None        return customer

The route can then focus on translating the service result into an HTTP response:

from typing import Annotatedfrom fastapi import Depends, FastAPI, HTTPExceptionapp = FastAPI()def get_customer_service() -> CustomerService:    repository = get_customer_repository()    return CustomerService(repository)CustomerServiceDep = Annotated[    CustomerService,    Depends(get_customer_service),]@app.get("/customers/{customer_id}")def read_customer(    customer_id: int,    service: CustomerServiceDep,):    customer = service.get_customer(customer_id)    if customer is None:        raise HTTPException(            status_code=404,            detail="Customer not found",        )    return customer

Here, get_customer_repository() represents application-specific repository construction. In a real application, it should be implemented using the chosen database and session-management strategy.

3. Manage resource lifetimes correctly

Database sessions, file handles, and other resources need predictable lifetimes. FastAPI dependencies can use yield to acquire a resource and clean it up afterward.

from collections.abc import Generatordef get_database_session() -> Generator:    session = create_session()    try:        yield session    finally:        session.close()

The example assumes create_session() comes from the application’s database setup. Production code should use the cleanup and asynchronous patterns appropriate to the database library.

Avoid creating a single database session at application startup and sharing it across concurrent requests. Session objects often represent mutable transactional state and should be managed according to the database library’s concurrency rules.

4. Use dependency overrides in tests

FastAPI allows tests to replace dependencies with test-specific implementations.

from fastapi.testclient import TestClientdef get_test_customer_service():    return FakeCustomerService()app.dependency_overrides[    get_customer_service] = get_test_customer_serviceclient = TestClient(app)def test_customer_endpoint():    response = client.get("/customers/42")    assert response.status_code == 200

FakeCustomerService should implement the behavior expected by the endpoint. Remember to clear dependency overrides after tests so that one test cannot accidentally influence another.

Overrides are particularly useful for replacing external APIs, authentication providers, and expensive infrastructure during unit or route-level tests.

5. Avoid excessive abstraction

Dependency injection is not a reason to create an interface, factory, repository, service, and adapter for every small operation. Introduce boundaries where they improve testing, isolate infrastructure, or represent meaningful business behavior.

A small read-only endpoint may need only a dependency and a straightforward query. A complex workflow involving multiple repositories and external services may benefit from a dedicated application service.

The goal is clarity, not the maximum number of layers.

Conclusion

FastAPI dependency injection makes shared behavior easier to compose and test. Use it to manage resources and application dependencies, keep HTTP handling separate from business logic, and avoid abstractions that do not solve a real problem.

Comments

No comments yet. Why don’t you start the discussion?

Leave a Reply

Your email address will not be published. Required fields are marked *