Chapter 5: Dependency Injection
Welcome back! In Chapter 4: OpenAPI & Automatic Docs, we saw how FastAPI automatically generates interactive documentation for our API, making it easy for others (and ourselves!) to understand and use. This works because FastAPI understands the structure of our paths, parameters, and Pydantic models.
Now, let’s explore another powerful feature that helps us write cleaner, more reusable, and better-organized code: Dependency Injection.
What Problem Does This Solve?
Imagine you’re building several API endpoints, and many of them need the same piece of information or the same setup step performed before they can do their main job. For example:
- Database Connection: Many endpoints might need to talk to a database. You need to get a database “session” or connection first.
- User Authentication: Many endpoints might require the user to be logged in. You need to check their credentials (like a token in a header) and fetch their user details.
- Common Parameters: Maybe several endpoints share common query parameters like
skip
andlimit
for pagination.
You could write the code to get the database session, check the user, or parse the pagination parameters inside each path operation function. But that would be very repetitive (violating the DRY - Don’t Repeat Yourself - principle) and hard to maintain. If you need to change how you get a database session, you’d have to update it in many places!
FastAPI’s Dependency Injection (DI) system provides an elegant solution to this. It allows you to define these common pieces of logic (like getting a user or a DB session) as separate, reusable functions called “dependencies”. Then, you simply “declare” that your path operation function needs the result of that dependency, and FastAPI automatically takes care of running the dependency and providing (“injecting”) the result into your function.
Our Goal Today: Learn how to use FastAPI’s Depends
function to manage dependencies, reuse code, and make our API logic cleaner and more modular.
Analogy: Think of your path operation function as the main chef preparing a dish (handling the request). Before the chef can cook, they might need specific ingredients prepared or tools set up. Dependency Injection is like having specialized assistants (dependencies):
- One assistant fetches fresh vegetables (e.g., gets common query parameters).
- Another assistant prepares the cooking station (e.g., gets a database session).
- Another assistant checks the order ticket to see who the dish is for (e.g., authenticates the user).
The chef simply tells the head waiter (Depends
) what they need (“I need prepared vegetables”, “I need the cooking station ready”), and the assistants automatically provide them just in time. The chef doesn’t need to know the details of how the vegetables were fetched or the station prepared; they just get the result.
Key Concepts
- Dependency: A function (or other callable) that provides some value needed by your path operation function (or even by another dependency). Examples: a function to get the current user, a function to connect to the database, a function to parse common query parameters.
Depends
: A special function imported fromfastapi
(from fastapi import Depends
) that you use in the parameters of your path operation function to signal that it requires a dependency. You use it like this:parameter_name: Annotated[ReturnType, Depends(dependency_function)]
.- Injection: FastAPI “injects” the result returned by the dependency function into the parameter of your path operation function. If
dependency_function()
returns the value10
, thenparameter_name
will be10
inside your path function. - Automatic Execution: FastAPI automatically figures out which dependencies are needed for a given request, calls them in the correct order (if dependencies depend on others), and manages their results.
- Reusability: Define a dependency once, and use
Depends(your_dependency)
in multiple path operations. - Caching (Per Request): By default, if a dependency is declared multiple times for the same request (e.g., if multiple path operation parameters need it, or if other dependencies need it), FastAPI will only run the dependency function once per request and reuse the result. This is efficient, especially for things like database connections or fetching user data. You can disable this cache if needed.
- Hierarchy: Dependencies can depend on other dependencies using
Depends
in their own parameters, forming a chain or tree of dependencies. FastAPI resolves this entire structure.
Using Dependencies: A Simple Example
Let’s start with a very common scenario: having shared query parameters for pagination.
-
Define the Dependency Function: Create a regular Python function that takes the parameters you want to share.
# common_dependencies.py (or within your router file) from typing import Annotated from fastapi import Query # This is our dependency function # It takes the common query parameters async def common_parameters( q: Annotated[str | None, Query(description="Optional query string")] = None, skip: Annotated[int, Query(description="Items to skip", ge=0)] = 0, limit: Annotated[int, Query(description="Max items to return", le=100)] = 100, ): # It simply returns a dictionary containing these parameters return {"q": q, "skip": skip, "limit": limit}
Explanation:
- This looks like a normal function that could handle path operation parameters.
- It takes
q
,skip
, andlimit
as arguments, usingQuery
for validation and documentation just like we learned in Chapter 2: Path Operations & Parameter Declaration. - It returns a dictionary containing the values it received. This dictionary will be the “result” injected into our path functions.
-
Use
Depends
in Path Operations: Now, importDepends
and your dependency function, and use it in your path operation parameters.# routers/items.py (example) from typing import Annotated from fastapi import APIRouter, Depends # Assume common_parameters is defined in common_dependencies.py from ..common_dependencies import common_parameters router = APIRouter() # Fake data for demonstration fake_items = [{"item_name": "Foo"}, {"item_name": "Bar"}, {"item_name": "Baz"}] @router.get("/items/") # Here's the magic! Declare 'commons' parameter using Depends async def read_items( commons: Annotated[dict, Depends(common_parameters)] # Dependency Injection! ): # Inside this function, 'commons' will be the dictionary returned # by common_parameters after FastAPI calls it with the query params. print(f"Received common parameters: {commons}") # Use the values from the dependency q = commons["q"] skip = commons["skip"] limit = commons["limit"] response_items = fake_items[skip : skip + limit] if q: response_items = [item for item in response_items if q in item["item_name"]] return response_items @router.get("/users/") # We can reuse the SAME dependency here! async def read_users( commons: Annotated[dict, Depends(common_parameters)] # Reusing the dependency ): # 'commons' will again be the dict returned by common_parameters print(f"Received common parameters for users: {commons}") # Imagine fetching users using commons['skip'], commons['limit']... return {"message": "Users endpoint", "params": commons}
Explanation:
from fastapi import Depends
: We importDepends
.from ..common_dependencies import common_parameters
: We import our dependency function.commons: Annotated[dict, Depends(common_parameters)]
: This is the key part!- We declare a parameter named
commons
. - Its type hint is
dict
(because our dependency returns a dictionary). Technically, FastAPI infers the type from the dependency function’s return type hint if available, but explicitly addingdict
here helps clarity. For more complex types, use the exact return type. - We wrap the type hint and
Depends(common_parameters)
inAnnotated
. This is the standard way to useDepends
. Depends(common_parameters)
tells FastAPI: “Before runningread_items
, call thecommon_parameters
function. Take the query parametersq
,skip
,limit
from the incoming request, pass them tocommon_parameters
, get its return value, and assign it to thecommons
variable.”
- We declare a parameter named
- Reusability: Notice how
read_users
uses the exact same dependency declarationAnnotated[dict, Depends(common_parameters)]
. We didn’t have to repeat theq
,skip
,limit
definitions.
How it Behaves:
- Run your app (
uvicorn main:app --reload
, assumingmain.py
includes this router). - Visit
http://127.0.0.1:8000/items/?skip=1&limit=1
.- FastAPI sees
Depends(common_parameters)
. - It extracts
skip=1
andlimit=1
(andq=None
) from the query string. - It calls
common_parameters(q=None, skip=1, limit=1)
. common_parameters
returns{"q": None, "skip": 1, "limit": 1}
.- FastAPI calls
read_items(commons={"q": None, "skip": 1, "limit": 1})
. - You see the print statement and get the response
[{"item_name":"Bar"}]
.
- FastAPI sees
- Visit
http://127.0.0.1:8000/users/?q=test
.- FastAPI calls
common_parameters(q="test", skip=0, limit=100)
. common_parameters
returns{"q": "test", "skip": 0, "limit": 100}
.- FastAPI calls
read_users(commons={"q": "test", "skip": 0, "limit": 100})
. - You see the print statement and get the JSON response.
- FastAPI calls
Dependencies Can Depend on Other Dependencies
The real power comes when dependencies themselves need other dependencies. Let’s sketch a simplified example for getting an item from a fake database.
-
Define a “DB Session” Dependency: (This will be fake, just returning a string).
# common_dependencies.py async def get_db_session(): print("Getting DB Session") # In reality, this would connect to a DB and yield/return a session object session = "fake_db_session_123" # You might use 'yield' here for setup/teardown (see FastAPI docs) return session
-
Define a Dependency that Uses the DB Session:
# common_dependencies.py from typing import Annotated from fastapi import Depends, HTTPException # Import the DB session dependency from .common_dependencies import get_db_session async def get_item_from_db( item_id: int, # Takes a regular path parameter db: Annotated[str, Depends(get_db_session)] # Depends on get_db_session! ): print(f"Getting item {item_id} using DB session: {db}") # Fake database interaction fake_db = {1: "Item One", 2: "Item Two"} if item_id not in fake_db: raise HTTPException(status_code=404, detail="Item not found in DB") return fake_db[item_id]
Explanation:
get_item_from_db
takes a regularitem_id
(which FastAPI will get from the path).- It also takes
db: Annotated[str, Depends(get_db_session)]
. It declares its own dependency onget_db_session
. - When FastAPI needs to run
get_item_from_db
, it first sees theDepends(get_db_session)
. It runsget_db_session
, gets"fake_db_session_123"
, and then callsget_item_from_db(item_id=..., db="fake_db_session_123")
.
-
Use the High-Level Dependency in a Path Operation:
# routers/items.py # ... other imports ... from ..common_dependencies import get_item_from_db @router.get("/db_items/{item_id}") # This endpoint depends on get_item_from_db async def read_db_item( item_id: int, # Path parameter for get_item_from_db item_name: Annotated[str, Depends(get_item_from_db)] # Inject result here! ): # 'item_name' will be the string returned by get_item_from_db # after it used the result from get_db_session. return {"item_id": item_id, "name_from_db": item_name}
Explanation:
- The
read_db_item
function only needs to declareDepends(get_item_from_db)
. - FastAPI automatically handles the whole chain:
read_db_item
->get_item_from_db
->get_db_session
. - Notice the
item_id: int
path parameter is declared in bothread_db_item
andget_item_from_db
. FastAPI is smart enough to pass the path parameter value to the dependency that needs it.
- The
Caching in Action:
If get_db_session
was also needed directly by read_db_item
(e.g., db_session: Annotated[str, Depends(get_db_session)]
), FastAPI would still only call get_db_session
once for the entire request to /db_items/{item_id}
because of the default caching (use_cache=True
in Depends
). The result "fake_db_session_123"
would be shared.
How it Works Under the Hood (Simplified)
Let’s trace a request to /db_items/2
using the example above:
- Request: Client sends
GET /db_items/2
. - Routing: FastAPI matches the request to the
read_db_item
path operation function. - Dependency Analysis: FastAPI inspects the signature of
read_db_item
:item_id: int
-> Needs value from path. Value is2
.item_name: Annotated[str, Depends(get_item_from_db)]
-> Needs the result ofget_item_from_db
.
- Solving
get_item_from_db
: FastAPI inspectsget_item_from_db
:item_id: int
-> Needs a value. FastAPI seesitem_id
is also needed by the parent (read_db_item
) and comes from the path. Value is2
.db: Annotated[str, Depends(get_db_session)]
-> Needs the result ofget_db_session
.
- Solving
get_db_session
: FastAPI inspectsget_db_session
:- It has no parameters.
- Checks cache: Has
get_db_session
run for this request? No. - Calls
get_db_session()
. It prints “Getting DB Session” and returns"fake_db_session_123"
. - Stores
get_db_session
->"fake_db_session_123"
in the request cache.
- Calling
get_item_from_db
: FastAPI now has the dependencies forget_item_from_db
:item_id
=2
(from path)db
="fake_db_session_123"
(fromget_db_session
result)- Calls
get_item_from_db(item_id=2, db="fake_db_session_123")
. - It prints “Getting item 2 using DB session: fake_db_session_123”, looks up
2
in its fake DB, and returns"Item Two"
. - Stores
get_item_from_db
->"Item Two"
in the request cache.
- Calling
read_db_item
: FastAPI now has the dependencies forread_db_item
:item_id
=2
(from path)item_name
="Item Two"
(fromget_item_from_db
result)- Calls
read_db_item(item_id=2, item_name="Item Two")
.
- Response: The function returns
{"item_id": 2, "name_from_db": "Item Two"}
, which FastAPI sends back to the client as JSON.
Here’s a simplified sequence diagram:
sequenceDiagram
participant Client
participant FastAPIApp as FastAPI App
participant DepSolver as Dependency Solver
participant GetItemFunc as get_item_from_db
participant GetDBFunc as get_db_session
participant PathOpFunc as read_db_item
Client->>+FastAPIApp: GET /db_items/2
FastAPIApp->>+DepSolver: Solve dependencies for read_db_item(item_id, Depends(get_item_from_db))
DepSolver->>DepSolver: Need path param 'item_id' (value=2)
DepSolver->>DepSolver: Need result of get_item_from_db
DepSolver->>+DepSolver: Solve dependencies for get_item_from_db(item_id, Depends(get_db_session))
DepSolver->>DepSolver: Need 'item_id' (value=2, from path)
DepSolver->>DepSolver: Need result of get_db_session
DepSolver->>DepSolver: Check cache for get_db_session: Miss
DepSolver->>+GetDBFunc: Call get_db_session()
GetDBFunc-->>-DepSolver: Return "fake_db_session_123"
DepSolver->>DepSolver: Cache: get_db_session -> "fake_db_session_123"
DepSolver-->>-DepSolver: Dependencies for get_item_from_db ready
DepSolver->>+GetItemFunc: Call get_item_from_db(item_id=2, db="fake_db_session_123")
GetItemFunc-->>-DepSolver: Return "Item Two"
DepSolver->>DepSolver: Cache: get_item_from_db -> "Item Two"
DepSolver-->>-FastAPIApp: Dependencies for read_db_item ready
FastAPIApp->>+PathOpFunc: Call read_db_item(item_id=2, item_name="Item Two")
PathOpFunc-->>-FastAPIApp: Return {"item_id": 2, "name_from_db": "Item Two"}
FastAPIApp-->>-Client: Send JSON Response
Code Connections
fastapi.Depends
(fastapi/param_functions.py
): This class is mostly a marker. When FastAPI analyzes function parameters, it looks for instances ofDepends
.fastapi.dependencies.utils.get_dependant
: This crucial function takes a callable (like your path operation function or another dependency) and inspects its signature. It identifies which parameters are path/query/body parameters and which are dependencies (marked withDepends
). It builds aDependant
object representing this.fastapi.dependencies.models.Dependant
: A data structure (dataclass) that holds information about a callable: its name, the callable itself, its path/query/header/cookie/body parameters, and importantly, a list of otherDependant
objects for its sub-dependencies. This creates the dependency tree/graph.fastapi.dependencies.utils.solve_dependencies
: This is the engine that recursively traverses theDependant
graph for a given request. It figures out the order, checks the cache (dependency_cache
), calls the dependency functions (usingrun_in_threadpool
for sync functions or awaiting async ones), handles results from generators (yield
), and gathers all the computed values needed to finally call the target path operation function.
FastAPI intelligently combines Python’s introspection capabilities with this structured dependency resolution system.
Conclusion
You’ve learned about FastAPI’s powerful Dependency Injection system!
- You saw how to define reusable logic in dependency functions.
- You learned to use
Depends
in your path operation function parameters to tell FastAPI what dependencies are needed. - You understood that FastAPI automatically calls dependencies and injects their results into your function.
- You saw how dependencies can depend on other dependencies, creating manageable hierarchies.
- You learned that results are cached per request by default for efficiency.
- You grasped the core idea: separating concerns and promoting reusable code.
Dependency Injection is fundamental to building complex, maintainable applications in FastAPI. It’s used extensively for things like database connections, authentication, authorization, and processing complex parameter sets.
While dependencies help manage complexity, sometimes things inevitably go wrong – a database might be unavailable, validation might fail within a dependency, or unexpected errors might occur. How should our API handle these situations gracefully? That’s what we’ll cover next.
Ready to handle errors like a pro? Let’s move on to Chapter 6: Error Handling!
Generated by AI Codebase Knowledge Builder