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.