Welcome back! In our last lesson, you successfully converted a synchronous FastAPI application to be fully asynchronous, learning how to handle I/O-bound database operations without blocking the event loop. A key piece of syntax we used was db: AsyncSession = Depends(database.get_db).
This lesson zooms in on that exact mechanism: FastAPI's powerful Dependency Injection (DI) system. We will explore what DI is conceptually, why it is a cornerstone of robust and scalable software architecture, and how to use it specifically to manage shared resources like database connections. Mastering this will make your code cleaner, more modular, and significantly easier to test.
1. What is Dependency Injection?
At its core, Dependency Injection is a design pattern that implements a principle called Inversion of Control (IoC). Instead of a function or object creating its own dependencies (the things it needs to work, like a database session or an external API client), those dependencies are "injected" from an external source.
This might sound abstract, so let's use an analogy. Imagine a chef in a kitchen (our API endpoint function).
- Without DI: The chef has to go to the storeroom, find the ingredients, and prepare them every time they cook a dish. If the storeroom location changes, every recipe the chef knows is broken. This is tightly coupled.
- With DI: A kitchen assistant (FastAPI's DI system) brings the prepared ingredients (the dependencies) directly to the chef's station just when they are needed. The chef doesn't need to know where the ingredients came from, only that they are available. This is loosely coupled.
The benefits are immense:
- Modularity & Reusability: The chef's recipe is simpler and can be reused in any kitchen with a competent assistant.
- Testability: To test the recipe, you can just give the chef pre-measured, specific test ingredients instead of needing a whole storeroom.
- Maintainability: If the storeroom moves, you only need to update the assistant's instructions, not every single recipe.
To get a great visual and code-based explanation of this concept, the following video is an excellent starting point.
Dependency Injection Explained Like You’re 5 (with FastAPI Examples)
The video "Dependency Injection Explained Like You’re 5" by Eric Roby provides a clear, concise introduction to the "what" and "why" of DI using simple FastAPI examples.
Please watch the first part of the video covering the core concepts. Focus on the distinction between tightly coupled and loosely coupled applications. Notice how DI promotes loose coupling.
2. The get_db Pattern for Database Sessions
In the previous lesson, we created a get_db function. This is the most common and critical use case for dependency injection in a database-backed application. The purpose of this function is not just to provide a session, but to manage its lifecycle: create it at the start of a request and guarantee it's closed at the end, even if errors occur.
For our async application, the modern pattern uses an async with block, which is clean and effective.
This resource from LobeHub shows a production-ready, async-first pattern for setting up a database connection and dependency provider in FastAPI.
First, study the code in the "Create database.py" section. This is a perfect, modern implementation of the async get_db function we need. Pay close attention to the async def get_db() function: async with async_session_factory() as session: This line creates a new AsyncSession. yield session This is the magic. It "yields" the session to your API endpoint. Your endpoint code runs here. finally: await session.close() This part runs after your endpoint has finished and sent its response. It ensures the database session is always closed, returning its connection to the pool. The try...finally block guarantees closure even if an exception happens in your endpoint. Next, scroll down to the "Key Configuration Options" tables. The explanations for expire_on_commit=False and autoflush=False are particularly important for async contexts.
The DI system ensures this setup and teardown happens for every single request that needs a database connection, without you having to write repetitive try...finally blocks in every endpoint.

3. Injecting and Using the Session in Endpoints
Now that we have a robust get_db dependency provider, let's see how to use it. You'll declare the dependency in your path operation function's signature. FastAPI sees this and automatically calls get_db for you.
The following video, though using a synchronous setup, perfectly demonstrates the pattern of injecting and then using the database session. The logic is identical for our async case; we would just add await before database calls.
Python FastAPI Tutorial (Part 5): Adding a Database - SQLAlchemy Models and Relationships
This tutorial by Corey Schafer provides a practical walkthrough of injecting a database session and using it to perform CRUD operations.
First, watch how the dependency is declared using Annotated from this timestamp. Annotated[Session, Depends(get_db)] is the modern, more explicit way to write db: Session = Depends(get_db). It improves readability and is supported by type checkers. Next, watch the implementation of the create_user endpoint from this segment. You don't need to code along, but observe the pattern: The db object is received as a function parameter. It's used to perform database operations: db.execute, db.add, db.commit, db.refresh. The function logic is clean, focusing only on the business task, not on connection management.
Here is what a complete async example looks like, combining what you've learned:
# In database.py
from sqlalchemy.ext.asyncio import create_async_engine, async_sessionmaker, AsyncSession
from collections.abc import AsyncGenerator
DATABASE_URL = "postgresql+asyncpg://user:password@host/db"
engine = create_async_engine(DATABASE_URL)
async_session_factory = async_sessionmaker(engine, expire_on_commit=False)
async def get_db() -> AsyncGenerator[AsyncSession, None]:
async with async_session_factory() as session:
yield session
# In main.py
from fastapi import FastAPI, Depends
from sqlalchemy.ext.asyncio import AsyncSession
from typing import Annotated
from . import database, crud, schemas
app = FastAPI()
@app.post("/users/", response_model=schemas.User)
async def create_user(
user: schemas.UserCreate,
db: Annotated[AsyncSession, Depends(database.get_db)]
):
# The 'db' session is provided by FastAPI's DI system
db_user = await crud.get_user_by_email(db=db, email=user.email)
if db_user:
raise HTTPException(status_code=400, detail="Email already registered")
return await crud.create_user(db=db, user=user)
4. Best Practices for Organizing Dependencies
As your application grows, you'll create more dependencies (e.g., for getting the current authenticated user, for clients to other services). It's a best practice to keep these provider functions in a dedicated module.

A common convention is to create a deps.py or dependencies.py file. This centralizes your dependency logic, making the system easier to understand and manage.
Injecting The Database Session Dependency
This article by Christopher GS demonstrates this organizational practice clearly.
Read the section titled "Injecting The Database Session Dependency". Note how the author places the get_db function in a file at app/api/deps.py and then imports it for use in the endpoint file. This separation of concerns is a hallmark of a well-structured application.
Conclusion
In this lesson, you've moved from simply using FastAPI's Depends to deeply understanding why and how it works. You now see dependency injection not as a piece of framework magic, but as a fundamental software design pattern that enables you to build scalable, maintainable, and testable applications.
Key Takeaways:
- Dependency Injection (DI) is a pattern where an object's dependencies are provided to it externally, promoting loose coupling.
- FastAPI's
Dependssystem is a powerful implementation of DI. - The most common use case is managing the lifecycle of a database session.
- The
get_dbdependency provider should use atry...finallyblock (or anasync withcontext manager) to ensure resources like database connections are always released. - Using
Annotated[Type, Depends(provider)]is the modern and recommended syntax for declaring dependencies. - Organizing dependency providers in a dedicated
deps.pyfile keeps your project clean and maintainable.
Preview of the Next Lesson
Our API endpoints can now efficiently handle incoming requests and interact with the database. But what if a request triggers a long-running process, like sending a confirmation email or generating a report? Making the user wait for that to complete is a poor experience. In the next lesson, we will explore FastAPI's background tasks, a mechanism to offload this kind of work to be done after the response has already been sent to the client.