Hello! Welcome to the next lesson in our journey through FastAPI's Dependency Injection (DI) system.
In our last session, we built reusable, parameterized dependencies using callable classes, creating a RoleChecker that could be configured for different roles. We saw that our RoleChecker itself had a dependency on get_current_user, giving us a glimpse of today's topic.
This lesson directly addresses the learning outcome: Chain nested dependencies to compose complex, reusable logic. We'll explore how FastAPI intelligently resolves entire "graphs" of dependencies, allowing us to build powerful, clean, and highly modular application logic by composing small, single-purpose functions. This is a core pattern for building scalable and maintainable APIs in FastAPI.
For you, coming from Django REST Framework, this concept is analogous to how you might combine multiple permission classes or how middleware processes a request. However, you'll see that FastAPI's explicit, graph-based approach offers a unique level of flexibility and clarity.
1. Understanding the Dependency Graph
When a dependency requires another dependency, FastAPI builds an internal dependency graph. It automatically figures out the correct order of execution, resolves each dependency, and injects the results where they're needed. It's designed to be intuitive—you simply ask for what you need in your function signature, and FastAPI handles the rest.
To see this in action, let's start with the official documentation, which explains this concept with a simple, clear example.
This page from the FastAPI documentation, 'Sub-dependencies', introduces the core concept of how dependencies can depend on other dependencies. It shows how FastAPI resolves a simple chain and visualizes it as a graph.
Please read from the beginning down to the section 'Use the dependency'. Pay close attention to how the query_or_cookie_extractor dependency itself depends on query_extractor, and how the final path operation only needs to declare the top-level dependency.
As you saw, the path operation read_query only declares one dependency, query_or_cookie_extractor. FastAPI inspects that function, sees that it needs query_extractor, and executes query_extractor first. The return value from query_extractor is then passed as an argument to query_or_cookie_extractor.
This creates a dependency chain:read_query -> query_or_cookie_extractor -> query_extractor
2. A Practical Example: Chaining Authorization Logic
The documentation's example is great for understanding the mechanism, but let's look at a more realistic use case that builds on what we've learned about authentication and authorization.
A common pattern is to have a base dependency that gets the current authenticated user, and then have other dependencies that use this user object to perform more specific authorization checks.
The Ultimate FastAPI Tutorial Part 11 - Dependency Injection via ...
This section of ChristopherGS's 'Ultimate FastAPI Tutorial' provides an excellent, practical example of a sub-dependency for authorization. It shows how to build a get_current_superuser dependency that chains from an existing get_current_user dependency.
Read the section titled 'Using FastAPI Depends Sub-Dependencies - Auth Example'. Focus on the code for get_current_active_superuser and how it takes current_user (which is itself injected via Depends) as a parameter.
This example perfectly illustrates the power of composition. Instead of writing a monolithic function that authenticates a user and checks if they are a superuser, the logic is split into two distinct, reusable parts:
get_current_user: Responsible only for authenticating the user (e.g., from a JWT).get_current_active_superuser: Responsible only for the authorization check, assuming it has been given an authenticated user.
This separation makes your code cleaner, easier to test, and more composable. You could easily create another dependency, say get_current_editor, that also depends on get_current_user but checks for a different role.
3. Building a Multi-level Dependency Chain for Resource Ownership
Let's construct our own, slightly more complex chain. A very common API requirement is to verify that a user not only is authenticated but also owns the specific resource they are trying to modify.
This involves three steps:
- Authentication: Get the current user.
- Data Retrieval: Get the resource from the database by its ID.
- Authorization: Check if the user's ID matches the resource's owner ID.
We can model each of these steps as a separate dependency.
First, let's set up some mock data and models.
from fastapi import Depends, FastAPI, HTTPException, status, Path
from pydantic import BaseModel
from typing import Annotated
# --- Models ---
class User(BaseModel):
id: int
username: str
class Item(BaseModel):
id: int
name: str
owner_id: int
# --- Mock Database ---
mock_items_db = {
1: Item(id=1, name="Foo", owner_id=1),
2: Item(id=2, name="Bar", owner_id=1),
3: Item(id=3, name="Baz", owner_id=2),
}
# A "logged in" user
mock_current_user = User(id=1, username="testuser")
# --- Dependencies ---
# Level 1 Dependency: Authentication
async def get_current_user() -> User:
# In a real app, this would decode a JWT from the request headers
return mock_current_user
# Level 2 Dependency: Data Retrieval
async def get_item_by_id(item_id: Annotated[int, Path()]) -> Item:
item = mock_items_db.get(item_id)
if not item:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Item not found")
return item
Now, we can create the top-level dependency that chains the first two together to perform the authorization check.
# Level 3 Dependency: Authorization
async def get_owned_item(
current_user: Annotated[User, Depends(get_current_user)],
item: Annotated[Item, Depends(get_item_by_id)]
) -> Item:
if item.owner_id != current_user.id:
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Not authorized to modify this item")
return item
Notice how get_owned_item is incredibly clear about its needs: it requires a current_user and an item. It doesn't care how FastAPI gets them; it just knows it will receive them.
Finally, the path operation becomes beautifully simple. It only has to declare its ultimate need: an item that is confirmed to be owned by the current user.
app = FastAPI()
@app.patch("/items/{item_id}")
async def update_item(
# The endpoint only needs to depend on the final, composed dependency
item_to_update: Annotated[Item, Depends(get_owned_item)],
# ... payload to update the item would go here
):
# By the time this code runs, we are GUARANTEED to have an item
# that exists and is owned by the logged-in user.
return {"message": f"Successfully updated item: {item_to_update.name}"}
This is a powerful separation of concerns. The endpoint update_item contains only business logic. All the boilerplate of authentication, data fetching, and authorization is handled by the dependency graph.
4. Automatic Caching and Efficiency
What happens if multiple dependencies in the same request need the same sub-dependency? For example, what if you had another dependency, log_user_activity, that also depended on get_current_user?
# Another dependency that also needs the user
async def log_user_activity(user: Annotated[User, Depends(get_current_user)]):
print(f"User {user.username} is accessing the API.")
@app.patch("/items/{item_id}")
async def update_item(
item_to_update: Annotated[Item, Depends(get_owned_item)],
_log_activity: Annotated[None, Depends(log_user_activity)] # Added for example
):
...
You might worry that get_current_user would be executed twice. It won't.
FastAPI automatically caches the results of dependencies for a single request.
When FastAPI builds the dependency graph for a request, it sees that both get_owned_item and log_user_activity need get_current_user. It will execute get_current_user once, store its result (the User object), and pass that same object to both functions that need it. This is extremely efficient and prevents redundant operations like decoding the same JWT multiple times or re-fetching the same user from a database within the same request.
The FastAPI documentation page "Sub-dependencies" explains this in the section "Using the same dependency multiple times". In rare cases where you need to disable this behavior, you can use use_cache=False, but the default is almost always what you want.

Conclusion
Today we've seen how to leverage FastAPI's DI system to build clean, composable, and complex logic by chaining dependencies.
Key Takeaways:
- Dependency Graphs: FastAPI automatically resolves arbitrarily deep dependency trees. You declare what you need, and the framework figures out how to provide it.
- Composition over Monoliths: Chaining allows you to break down complex logic (like auth + fetch + authorization) into small, single-purpose, reusable, and testable functions.
- Clean Endpoints: Your path operation functions become lean and focused purely on business logic, as the dependencies handle all the preliminary checks and resource fetching.
- Automatic Caching: FastAPI efficiently reuses the results of dependencies within a single request, preventing redundant computations and database queries.
Preview of the Next Lesson:
So far, our dependencies have simply returned a value. But what if a dependency needs to acquire a resource before the endpoint runs (like opening a database connection) and then release it after the endpoint is finished (closing the connection)? This requires setup and teardown logic.
In our next lesson, "Create dependencies with yield for setup and teardown logic, such as managing database sessions," we will explore how to use yield to create dependencies that can manage the lifecycle of resources, a crucial pattern for any real-world application.