In this article, we will explore FastAPI's Dependency Injection system from first principles and understand how it works internally.
The Problem Dependency Injection Solves
Imagine a service that retrieves user information from a database. A straightforward implementation might look like this:from fastapi import FastAPI
app = FastAPI()
@app.get("/users/{user_id}")
async def get_user(user_id: int):
db = DatabaseConnection()
user = db.find_user(user_id)
return user
At first glance, this seems perfectly reasonable.
However, as applications grow, multiple endpoints often require access to shared resources such as database sessions, Redis connections, authenticated users, configuration objects, Kafka producers, and feature flag services.
If every endpoint creates these dependencies manually, business logic becomes tightly coupled to resource creation. Code duplication increases, testing becomes more difficult, and managing shared resources becomes harder.
A better approach is to separate resource creation from business logic. This is where Dependency Injection becomes useful.
Introducing Depends()
FastAPI provides the Depends() function to declare dependencies. Consider the following example:from fastapi import Depends
from fastapi import FastAPI
app = FastAPI()
class Database:
async def get_users(self):
return [
{"id": 1, "name": "John"},
{"id": 2, "name": "Alice"}
]
async def get_db():
return Database()
@app.get("/users")
async def get_users(
db: Database = Depends(get_db)
):
users = await db.get_users()
return users
Here, the endpoint does not create the database connection itself.
Instead, FastAPI executes the
get_db() dependency and injects the result into the endpoint. The endpoint receives a ready-to-use dependency. This keeps endpoint logic clean and focused.
A dependency can be a function, a class, or any callable object that FastAPI can execute to produce a value.
What Happens Internally?
When a request arrives, FastAPI does not immediately execute the endpoint.Instead, it follows a sequence of steps: Request Arrives → Route Matching → Dependency Resolution → Endpoint Execution → Response Returned.
Before the endpoint executes, FastAPI inspects all declared dependencies and resolves them. Only after dependency resolution completes does endpoint execution begin.

from fastapi import Depends
async def get_current_user():
return {
"id": 1,
"name": "John"
}
@app.get("/profile")
async def profile(
user = Depends(get_current_user)
):
return user
When a request reaches the endpoint, FastAPI performs the following operations:
1. Route matching occurs.
2. FastAPI identifies the dependency.
3. get_current_user() executes.
4. The dependency result is cached for the current request.
5. The result is injected into the endpoint.
6. The endpoint executes.
Nested Dependencies
One of the most powerful features of FastAPI's Dependency Injection system is support for nested dependencies. Consider the following example.from fastapi import Depends
async def get_db():
return "db"
async def get_repository(
db = Depends(get_db)
):
return f"repository({db})"
async def get_service(
repo = Depends(get_repository)
):
return f"service({repo})"
Endpoint:
@app.get("/users")
async def get_users(
service = Depends(get_service)
):
return service
FastAPI needs to execute not only get_service(), but also every dependency it relies on.
Since
get_service() depends on get_repository(), which in turn depends on get_db(), FastAPI automatically constructs a dependency graph and resolves all dependencies before executing the endpoint.
Dependencies are resolved from the deepest dependency to the endpoint. The execution order is:
get_db() → get_repository() → get_service() → Endpoint
Dependency Caching
Consider:async def get_db():
print("Creating database connection")
return "db"
Now suppose two dependencies require the same database connection.
async def get_repository(
db = Depends(get_db)
):
return db
async def get_service(
db = Depends(get_db)
):
return db
Although both dependencies require get_db(), FastAPI executes it only once per request.
The result is cached and reused whenever the same dependency is needed again, preventing duplicate dependency creation and improving efficiency.
By default, FastAPI caches dependency results for the lifetime of a single request.
Database Session Injection
One of the most common production use cases for Dependency Injection is database session management. Consider an async SQLAlchemy session.from sqlalchemy.ext.asyncio import AsyncSession
async def get_db():
async with SessionLocal() as session:
yield session
Endpoint:
@app.get("/users")
async def get_users(
db: AsyncSession = Depends(get_db)
):
return await service.get_users(db)
Here, FastAPI automatically provides a database session to the endpoint. The endpoint simply receives a ready-to-use database session without needing to know how it was created.
Dependencies that use
yield participate in FastAPI's automatic resource management. Consider:
async def get_db():
db = SessionLocal()
try:
yield db
finally:
db.close()
The code before yield creates the resource, and the yielded value is injected into the endpoint.
After the response is returned, FastAPI resumes the dependency and executes the code after
yield, allowing resources to be cleaned up automatically.
This pattern is commonly used for managing database sessions, file handles, network connections, and other external resources.
The request lifecycle becomes: Request → Dependency Resolution → Endpoint Execution → Response → Dependency Cleanup
This ensures resources are released correctly even when exceptions occur.
If a dependency raises an exception, endpoint execution never begins. This behavior is commonly used for authentication and authorization.
Conclusion
FastAPI's Dependency Injection system separates resource creation from business logic, making applications easier to maintain, test, and extend.By automatically resolving dependencies, supporting nested dependency graphs, caching dependency results, and managing resource lifecycles with
yield, FastAPI significantly reduces boilerplate code while promoting clean and reusable application design.