Module 1: Dependency Injection

Introduction to Module 1: Dependency Injection

Overview

Dependency Injection (DI) is FastAPI's most powerful feature for writing clean, reusable, testable code. It isn't a concept FastAPI invented — it's a design pattern that exists in frameworks like Spring (Java), NestJS (TypeScript), and Angular. But FastAPI implements it in an extraordinarily elegant way: with a single function and a single decorator. Depends() is all you need.

What problem does it solve? Repetition. Open the To-Do API from your FastAPI Fundamentals final project and look for patterns that repeat across endpoints. How many endpoints validate that a task_id exists and return 404 if it doesn't? How many parse skip and limit for pagination? How many reach into the same in-memory list of data? Every time you copy and paste that logic into a new endpoint, you're creating technical debt. DI lets you extract that logic into a separate function and tell FastAPI: "before running this endpoint, run this function and hand me the result."

This module transforms how you think about the structure of your code. It isn't about learning new syntax — Depends() is trivial. It's about learning to ask: "what logic is repeating?" and "how do I extract it so it's reusable and testable?"


Where are we in the guide?

You're in Module 1 of 6 of the FastAPI Advanced Features guide:

Module 1: Dependency Injection ← YOU ARE HERE
    → Depends(), sub-dependencies, yield dependencies, class-based

Module 2: APIRouter and Middleware
    → Modularization, routers, custom middleware, lifespan events

Module 3: Advanced Response Models
    → Multiple schemas, StreamingResponse, FileResponse

Module 4: Background Tasks
    → BackgroundTasks, asynchronous processing

Module 5: WebSockets and File Uploads
    → Real-time, file uploads with validation

Module 6: Project — Advanced Task Manager API
    → Integrating everything

Cumulative progression

This guide builds on FastAPI Fundamentals. Each module adds a professional layer:

FastAPI Fundamentals: CRUD + Pydantic + Error Handling + CORS
    ↓ (solid base)
Module 1: + Dependency Injection (DRY, testable code)  ← HERE
    ↓ (reusable logic)
Module 2: + APIRouter and Middleware (modular app)
    ↓ (professional structure)
Module 3: + Advanced Response Models (response control)
    ↓ (sophisticated responses)
Module 4: + Background Tasks (async processing)
    ↓ (background operations)
Module 5: + WebSockets and File Uploads (advanced features)
    ↓ (real-time communication)
Module 6: Task Manager API (full integration)

In FastAPI Fundamentals you learned to build a working API with CRUD, validation with Pydantic, error handling with HTTPException, and CORS. All of that works. But if you open your main.py, you probably see logic repeated across endpoints: the same ID validation, the same pagination parameters, the same data access. This module gives you the tools to eliminate that repetition.


What you already know vs what's new

What you already know

From FastAPI Fundamentals you bring:

  • ✅ Complete CRUD endpoints (GET, POST, PUT, PATCH, DELETE)
  • ✅ Pydantic models with BaseModel, Field(), @field_validator
  • ✅ Separate models: TaskCreate, TaskUpdate, TaskResponse
  • response_model to control responses
  • HTTPException for errors (404, 400, and so on)
  • ✅ Custom exception handlers
  • ✅ CORS configured with CORSMiddleware
  • ✅ Pagination with skip and limit
  • ✅ In-memory data (a list of dictionaries)
  • model_dump(), model_dump(exclude_unset=True) for PATCH

What's new in this module

  • 🆕 Depends() — injecting functions as dependencies into endpoints
  • 🆕 Dependency functions — extracting reusable logic (pagination, lookup, data access)
  • 🆕 Sub-dependencies — a dependency that uses another dependency
  • 🆕 Class-based dependencies — classes with __call__ for complex state
  • 🆕 Yield dependencies — setup/cleanup with yield (sessions, files, transactions)
  • 🆕 app.dependency_overrides — replacing dependencies for testing
  • 🆕 Router/app-level dependencies — not just per endpoint

From repeated code to DRY code

AspectWithout DI (your API today)With DI (after this module)
PaginationYou copy skip: int = 0, limit: int = 10 into every GETOne reusable pagination_params() function
Lookup by IDYou copy the lookup + 404 into GET, PUT, PATCH, DELETEOne get_task_or_404() function
Data accessYou reach directly into the global listOne function that returns the data source
TestingHard to mock the data accessdependency_overrides to inject test data
Common validationsYou copy validations into every endpointOne dependency that validates and returns

The problem: repeated code across endpoints

To understand why DI matters, let's look at a real example. This is a typical To-Do API as you come out of FastAPI Fundamentals:

from fastapi import FastAPI, HTTPException, Query
from pydantic import BaseModel, Field
from typing import Optional

app = FastAPI()

tasks = [
    {"id": 1, "title": "Buy groceries", "completed": False, "priority": "medium"},
    {"id": 2, "title": "Study FastAPI", "completed": False, "priority": "high"},
    {"id": 3, "title": "Work out", "completed": True, "priority": "low"},
]


class TaskCreate(BaseModel):
    title: str = Field(min_length=1, max_length=200)
    priority: str = Field(default="medium")


class TaskUpdate(BaseModel):
    title: Optional[str] = Field(default=None, min_length=1, max_length=200)
    completed: Optional[bool] = None
    priority: Optional[str] = None


@app.get("/tasks")
def list_tasks(skip: int = Query(default=0, ge=0), limit: int = Query(default=10, ge=1, le=100)):
    return tasks[skip : skip + limit]


@app.get("/tasks/{task_id}")
def get_task(task_id: int):
    for task in tasks:
        if task["id"] == task_id:
            return task
    raise HTTPException(status_code=404, detail=f"Task {task_id} not found")


@app.put("/tasks/{task_id}")
def update_task(task_id: int, task_data: TaskUpdate):
    for task in tasks:
        if task["id"] == task_id:
            update = task_data.model_dump(exclude_unset=True)
            task.update(update)
            return task
    raise HTTPException(status_code=404, detail=f"Task {task_id} not found")


@app.delete("/tasks/{task_id}")
def delete_task(task_id: int):
    for i, task in enumerate(tasks):
        if task["id"] == task_id:
            return tasks.pop(i)
    raise HTTPException(status_code=404, detail=f"Task {task_id} not found")

See the repetition? Three endpoints (get_task, update_task, delete_task) carry exactly the same logic:

for task in tasks:
    if task["id"] == task_id:
        # do something with task
        ...
raise HTTPException(status_code=404, detail=f"Task {task_id} not found")

That's 4 lines copied three times. If you add more endpoints that operate on a single task (mark as completed, set priority, add notes), you copy those 4 lines again. And if you change the error message or the lookup logic, you have to change it everywhere.

On top of that, skip and limit with their constraints repeat in every listing endpoint. If you decide to change the maximum limit from 100 to 50, you have to hunt down every endpoint that uses pagination.

This isn't a syntax problem. It's a design problem. And DI is the design solution.


The solution: Dependency Injection with Depends()

Dependency Injection means exactly what the name says: injecting a dependency. Instead of every endpoint running its own lookup logic, you extract that logic into a separate function and tell FastAPI to run it before the endpoint.

A quick preview

Here's the same code with DI:

from fastapi import FastAPI, HTTPException, Depends, Query

app = FastAPI()

tasks = [
    {"id": 1, "title": "Buy groceries", "completed": False, "priority": "medium"},
    {"id": 2, "title": "Study FastAPI", "completed": False, "priority": "high"},
    {"id": 3, "title": "Work out", "completed": True, "priority": "low"},
]


def pagination_params(
    skip: int = Query(default=0, ge=0),
    limit: int = Query(default=10, ge=1, le=100),
):
    return {"skip": skip, "limit": limit}


def get_task_or_404(task_id: int):
    for task in tasks:
        if task["id"] == task_id:
            return task
    raise HTTPException(status_code=404, detail=f"Task {task_id} not found")


@app.get("/tasks")
def list_tasks(pagination: dict = Depends(pagination_params)):
    skip = pagination["skip"]
    limit = pagination["limit"]
    return tasks[skip : skip + limit]


@app.get("/tasks/{task_id}")
def get_task(task: dict = Depends(get_task_or_404)):
    return task


@app.put("/tasks/{task_id}")
def update_task(task: dict = Depends(get_task_or_404), task_data: TaskUpdate = ...):
    update = task_data.model_dump(exclude_unset=True)
    task.update(update)
    return task


@app.delete("/tasks/{task_id}")
def delete_task(task: dict = Depends(get_task_or_404)):
    tasks.remove(task)
    return task

What changed?

  1. pagination_params is a function that encapsulates skip and limit. Defined once, used in every listing endpoint.

  2. get_task_or_404 encapsulates the lookup + 404. Defined once, used in get_task, update_task, and delete_task.

  3. Depends() tells FastAPI: "before running this endpoint, run this function and pass me the result as a parameter."

  4. The endpoints get simpler: get_task is literally return task. All the lookup and error logic already ran inside the dependency.

Why does it matter?

BenefitWithout DIWith DI
Changing the lookup logicModify 3+ endpointsModify 1 function
Changing paginationHunt down every endpoint with skip/limitModify 1 function
TestingMock every endpoint individuallyReplace the dependency with dependency_overrides
A new endpoint for a taskCopy 4 lines of lookupDepends(get_task_or_404)
ConsistencyEasy for one endpoint to end up with different logicA single source of truth

How Depends() works under the hood

When FastAPI sees task: dict = Depends(get_task_or_404) in an endpoint's signature, here's what it does:

1. Request arrives: DELETE /tasks/2
2. FastAPI reads delete_task's signature
3. It sees that task_id comes from the path parameter
4. It sees that task depends on get_task_or_404
5. It runs get_task_or_404(task_id=2) BEFORE the endpoint
6. If get_task_or_404 raises HTTPException → it responds 404, the endpoint does NOT run
7. If it returns a value → that value is passed as "task" to the endpoint
8. It runs delete_task(task=<the dict the dependency returned>)

The key is step 6: if the dependency raises an exception, the endpoint never runs. That's powerful, because it means that when your endpoint receives task, the task is guaranteed to exist. You don't need to check anything.

The mental pattern

Think of dependencies as preconditions. Before your endpoint does its job, certain things must be true:

  • "I need the pagination parameters" → Depends(pagination_params)
  • "I need the task to exist" → Depends(get_task_or_404)
  • "I need a database session" → Depends(get_db) (you'll see this in future guides)
  • "I need the user to be authenticated" → Depends(get_current_user) (the Auth guide)

Every Depends() is a declarative precondition. Your endpoint only says what it needs, not how to get it.


Dependency Injection in the real world

DI isn't an academic pattern. It's the production standard for professional APIs. These are the most common use cases:

1. Database access

def get_db():
    db = SessionLocal()
    try:
        yield db
    finally:
        db.close()

@app.get("/tasks")
def list_tasks(db: Session = Depends(get_db)):
    return db.query(Task).all()

Every endpoint receives a clean database session. When it finishes, the session closes automatically. This is covered in the Database Integration guide.

2. Authentication

def get_current_user(token: str = Header(...)):
    user = verify_token(token)
    if not user:
        raise HTTPException(status_code=401, detail="Invalid token")
    return user

@app.get("/tasks")
def list_tasks(user: dict = Depends(get_current_user)):
    return get_tasks_for_user(user["id"])

Every protected endpoint declares that it needs an authenticated user. The token verification logic lives in exactly one place. This is covered in the Authentication & Authorization guide.

3. Rate limiting

def rate_limiter(request: Request):
    client_ip = request.client.host
    if is_rate_limited(client_ip):
        raise HTTPException(status_code=429, detail="Too many requests")

@app.get("/tasks", dependencies=[Depends(rate_limiter)])
def list_tasks():
    ...

4. Configuration

def get_settings():
    return Settings()

@app.get("/tasks")
def list_tasks(settings: Settings = Depends(get_settings)):
    return get_tasks(max_results=settings.max_results)

In this module you focus on the fundamental patterns: pagination, lookup, and composing dependencies. The advanced patterns (database, auth, rate limiting) are built on exactly the same foundation.


What this module does NOT cover

It's important to know the boundaries:

  • Dependencies with a real database — You'll use in-memory data. SQLAlchemy + DI is covered in the Database Integration guide
  • Authentication dependenciesget_current_user, JWT tokens. Covered in Authentication & Authorization
  • Dependencies with external services — HTTP clients, third-party APIs. Covered in advanced guides
  • Full testing with DIdependency_overrides gets introduced, but deep testing goes in the Testing guide
  • Scoped dependencies (request scope) — An advanced pattern for large applications

Why these boundaries?

  • Database sessions are DI's flagship use case, but you need SQLAlchemy first. Mixing both concepts here would backfire
  • Auth with DI is fundamental, but you need to understand JWT/OAuth2 first. What you learn here is the foundation auth is built on
  • Testing with DI gets mentioned as motivation, but the full testing workflow goes in its own guide

The goal of this module is for you to master the DI mechanism with examples you can run right now, with no external dependencies.


Module objective

By the end of this module you'll be able to:

  • ✅ Explain what Dependency Injection is and which problems it solves
  • ✅ Create dependency functions and use them with Depends()
  • ✅ Extract repeated logic (pagination, lookup) into reusable dependencies
  • ✅ Create sub-dependencies (a dependency that depends on another)
  • ✅ Implement class-based dependencies with __call__
  • ✅ Use yield dependencies for setup/cleanup (resources with a lifecycle)
  • ✅ Apply dependencies at the endpoint, router, and app level
  • ✅ Use dependency_overrides to replace dependencies in testing
  • ✅ Refactor an existing API by applying DI

Prerequisites

For this module you need:

  • FastAPI Fundamentals completed — CRUD, Pydantic, HTTPException, CORS
  • Your To-Do API from the final project — With working CRUD endpoints
  • Python 3.9+ with FastAPI and uvicorn installed

Quick check

cd fastapi-advanced
source venv/bin/activate
uvicorn app.main:app --reload

If you don't have the project set up, spin one up quickly:

mkdir -p fastapi-advanced/app
cd fastapi-advanced
python -m venv venv
source venv/bin/activate
pip install "fastapi[standard]"

Create app/main.py with a basic app and check that http://localhost:8000/docs works.

What if you don't meet one of the prerequisites?

Missing prerequisiteWhat to do
You didn't complete FastAPI FundamentalsComplete at least the Pydantic and Error Handling modules
You don't have the To-Do APIUse the Books API from Fundamentals, or create a minimal API with 3-4 CRUD endpoints
You aren't on Pydantic v2Upgrade with pip install --upgrade pydantic — check with pip show pydantic
You don't remember HTTPExceptionReview the Error Handling module — you'll use it inside dependencies

Module roadmap

CapsuleTopicWhat you'll learn
01Introduction (this capsule)Context, the repetition problem, what DI is, roadmap
02Basic Depends()Creating dependency functions, pagination params, common filters
03Sub-dependencies and compositionDependency chains, class-based dependencies
04Yield dependencies and lifecycleSetup/cleanup, try/finally, dependency_overrides
05Project: Refactoring with DITransforming the To-Do API by applying DI throughout

Learning flow

First you'll learn to create basic dependency functions and use them with Depends() — pagination, common filters, item lookup (Capsule 02). Then you'll explore sub-dependencies, where one dependency uses another, and class-based dependencies for when you need configurable state (Capsule 03). Next you'll see yield dependencies for handling resources that need cleanup (sessions, files), and dependency_overrides for testing (Capsule 04). At the end, you'll pull it all together by refactoring your entire To-Do API with DI (Capsule 05).

The progression is: extract → compose → handle lifecycle → integrate.

Capsule by capsule

Capsule 02 — Basic Depends(): Your first real dependency. You'll create pagination_params(), which encapsulates skip and limit, and get_task_or_404(), which encapsulates the item lookup. You'll see how FastAPI resolves the dependency's parameters automatically — if your dependency takes task_id: int and the endpoint has {task_id} in the path, FastAPI wires the two together. You'll also explore common filters and dependencies that reach into the Request object.

Capsule 03 — Sub-dependencies and composition: When one dependency needs the result of another dependency. For example, get_task_with_permissions() needs get_current_user() first and then get_task_or_404(). You'll build dependency chains and understand the execution order. You'll also see class-based dependencies: classes with __call__ that let you configure a dependency with parameters (like a paginator with a configurable maximum limit).

Capsule 04 — Yield dependencies and lifecycle: FastAPI's most powerful dependencies. With yield, a dependency can do setup before the endpoint and cleanup after it — perfect for database connections, file handles, or transactions. You'll see the try/finally pattern that guarantees cleanup always happens. You'll also learn dependency_overrides to replace dependencies in testing without touching production code.

Capsule 05 — Project: Refactoring with DI: The full integration. You take your To-Do API and refactor it: pagination becomes a dependency, the task lookup becomes another, data access gets centralized. The result is an API with the same endpoints but significantly cleaner, more maintainable code.


The central concept: inversion of control

Dependency Injection is a form of inversion of control (IoC). In ordinary programming, your function controls which resources it uses and how it gets them:

# Direct control — the function decides how to get what it needs
@app.get("/tasks/{task_id}")
def get_task(task_id: int):
    task = find_task_in_list(tasks, task_id)  # the function knows where the data comes from
    if not task:
        raise HTTPException(404, detail="Not found")  # the function handles the error
    return task

With DI, you invert the control: the function declares what it needs, and the framework takes care of providing it:

# Inversion of control — the function receives what it needs
@app.get("/tasks/{task_id}")
def get_task(task: dict = Depends(get_task_or_404)):
    return task  # the function just does its job

The difference is subtle but profound:

  • Without DI: The endpoint knows how to look up tasks, where the data comes from, and how to handle the "not found" case. It's coupled to the implementation.
  • With DI: The endpoint only knows that it needs a task. How it's obtained, where it comes from, and what happens if it doesn't exist — all of that is the dependency's responsibility.

Why does inversion of control matter?

  1. Testing: You can replace get_task_or_404 with a function that always returns a test task. You don't need a real database to test your endpoint.

  2. Flexibility: If tomorrow you switch from in-memory data to PostgreSQL, you only change the dependency. The endpoints go untouched.

  3. Separation of concerns: The endpoint does one thing: its business logic. The dependency does another: obtaining the necessary resources.

  4. Composition: You can combine dependencies like LEGO bricks. get_task_with_permissions() uses get_current_user() + get_task_or_404().

The pattern in practice

The concept of inversion of control ties the whole module together:

  • Capsule 02: You extract logic into functions → you invert the control of "getting data"
  • Capsule 03: You compose dependencies → you invert the control of "checking preconditions"
  • Capsule 04: You handle lifecycle with yield → you invert the control of "managing resources"
  • Capsule 05: You apply IoC across your whole API → clean, decoupled code

When you think about DI, think about inversion of control. "Does my endpoint know too much about how to get what it needs?" is the question that guides you toward a better design.


Connection with the project

Your To-Do API is going to go through a deep refactor in this module. You aren't changing what the API does — you're changing how it does it:

Before (FastAPI Fundamentals) — repeated logic

@app.get("/tasks/{task_id}")
def get_task(task_id: int):
    for task in tasks:
        if task["id"] == task_id:
            return task
    raise HTTPException(status_code=404, detail=f"Task {task_id} not found")


@app.put("/tasks/{task_id}")
def update_task(task_id: int, data: TaskUpdate):
    for task in tasks:
        if task["id"] == task_id:
            task.update(data.model_dump(exclude_unset=True))
            return task
    raise HTTPException(status_code=404, detail=f"Task {task_id} not found")

After (this module) — reusable dependencies

def get_task_or_404(task_id: int) -> dict:
    for task in tasks:
        if task["id"] == task_id:
            return task
    raise HTTPException(status_code=404, detail=f"Task {task_id} not found")


@app.get("/tasks/{task_id}")
def get_task(task: dict = Depends(get_task_or_404)):
    return task


@app.put("/tasks/{task_id}")
def update_task(task: dict = Depends(get_task_or_404), data: TaskUpdate = ...):
    task.update(data.model_dump(exclude_unset=True))
    return task

The lookup logic gets written once. Every endpoint that needs a task says Depends(get_task_or_404) and receives the task, guaranteed.

The To-Do API as an evolving project

FastAPI Fundamentals M1-M3: API with basic CRUD (dicts)
    ↓
FastAPI Fundamentals M4: + Pydantic (validation)
    ↓
FastAPI Fundamentals M5: + Error Handling + CORS (robustness)
    ↓
FastAPI Fundamentals M6: To-Do API (integrating project)
    ↓
Advanced Features M1: + Dependency Injection (DRY)  ← HERE
    ↓
Advanced Features M2: + APIRouter + Middleware (modular)
    ↓
Advanced Features M3-M5: + Advanced features
    ↓
Advanced Features M6: Task Manager API (advanced project)

How to use this guide in Module 1

A practical approach

Every capsule includes code you should write and run. The cycle for this module is:

  1. Spot the repetition — Look at your current code and find repeated patterns
  2. Extract into a function — Move the repeated logic into a dependency function
  3. Use Depends() — Replace the inline logic with Depends(your_function)
  4. Try it in /docs — Check that the endpoint works the same as before
  5. Check the error case — Confirm that the dependency's exceptions work

Estimated time

  • Introduction capsule (this one): 15-20 minutes
  • Technical capsules (02-04): 35-50 minutes each
  • Project (05): 60-90 minutes
  • Module 1 total: 3-4 hours

A refactoring tip

When you refactor an endpoint to use DI, do it in two steps: first, create the dependency function with the extracted logic and check that it works on its own. Second, modify the endpoint to use Depends(). If something breaks, you know the problem is in how you wired the dependency, not in the logic itself.


Signs of success

By the end of this module (all 5 capsules), you'll know you succeeded if:

  • ✅ You have at least 3 dependency functions: pagination, item lookup, and one more
  • ✅ No endpoint repeats lookup + 404 logic — it all goes through get_task_or_404
  • ✅ The pagination parameters are defined in exactly one place
  • ✅ You have at least one sub-dependency (a dependency that uses another)
  • ✅ You have at least one class-based dependency with __call__
  • ✅ You understand yield dependencies and when to use them
  • ✅ You know how to use dependency_overrides for testing
  • ✅ Your To-Do API works exactly as it did before, but with less repeated code
  • ✅ You can explain the difference between a dependency and a helper function
  • ✅ You understand why DI is fundamental for auth, database, and testing

Summary

  • This is Module 1 of 6, focused on Dependency Injection
  • DI solves the problem of repeated code across endpoints: pagination, lookup, data access
  • Depends() is FastAPI's mechanism for injecting dependencies — simple but powerful
  • A dependency function is a regular Python function that FastAPI runs before the endpoint
  • If the dependency raises an exception, the endpoint doesn't run — guaranteed preconditions
  • DI is inversion of control: the endpoint declares what it needs, not how to get it
  • Patterns covered: basic functions, sub-dependencies, class-based, yield
  • DI is the foundation of auth, database sessions, and testing — what you learn here gets used across the whole path
  • Not covered: a real database, auth, deep testing — those come in later guides

Additional resources

  1. FastAPI - Dependencies — The official Dependency Injection tutorial for FastAPI
  2. FastAPI - Dependencies with yield — Yield dependencies for setup/cleanup
  3. FastAPI - Classes as Dependencies — Class-based dependencies
  4. FastAPI - Sub-dependencies — Dependencies that use other dependencies
  5. FastAPI - Global Dependencies — App- and router-level dependencies
  6. Martin Fowler - Inversion of Control — The IoC concept explained by Martin Fowler

What's next?

Next capsule: Basic Depends() — You'll create your first dependency function for pagination, another for item lookup, and you'll see how FastAPI automatically resolves your dependencies' parameters. The copy-paste between endpoints ends here.