Lesson 1 - Dependency Injection

Welcome to Dependency Injection

As an API grows, the same little jobs show up in endpoint after endpoint: read the pagination parameters, check the caller is authenticated, open a database connection. Copy-pasting that logic into every function is how codebases rot — change one rule and you have to hunt down ten copies. FastAPI’s answer is dependency injection: you write the shared logic once as a dependency, declare which endpoints need it with Depends, and FastAPI runs it for you and hands the result to your function. It’s one of FastAPI’s most loved features, and once it clicks you’ll use it constantly.

This lesson introduces the core idea. The next lesson adds class dependencies, sub-dependencies, and setup/teardown with yield.

By the end of this lesson, you will be able to:

  • Explain what dependency injection is and the problem it solves
  • Write a dependency function and inject it with Depends
  • Reuse one dependency across multiple endpoints
  • Describe why this keeps code DRY, testable, and consistent

You’ll build on the endpoints from earlier modules. Let’s begin.


The Problem: Repeated Logic

Imagine three list endpoints — tasks, users, projects — that all support skip and limit for pagination. Without dependency injection, each one re-declares the same parameters:

@app.get("/tasks")
def list_tasks(skip: int = 0, limit: int = 10):
    ...

@app.get("/users")
def list_users(skip: int = 0, limit: int = 10):
    ...

It’s harmless with two endpoints and two parameters. But add a third parameter, or a rule like “limit can’t exceed 100,” and you’re editing every endpoint by hand — and eventually they drift out of sync. The logic wants to live in one place.


A Dependency Is Just a Function

A dependency in FastAPI is simply a function that produces something an endpoint needs. Here’s one for pagination — it takes the same query parameters and returns them as a dictionary:

def pagination(skip: int = 0, limit: int = 10):
    return {"skip": skip, "limit": limit}

There’s nothing special about this function — it uses the exact query-parameter rules you learned in Module 1. The magic is in how you use it: instead of repeating skip and limit in each endpoint, you declare that the endpoint depends on pagination.

A diagram showing one 'dependency' function, def pagination(skip=0, limit=10), at the top. Three endpoints below it — GET /tasks, GET /users, and GET /projects — each declare Depends(pagination), with arrows pointing up to the shared dependency. A note explains FastAPI runs the dependency before your endpoint and passes the result in, so shared logic lives in one place.
Each endpoint just declares what it needs with Depends(...); no copy-paste, and easy to test or swap.

Injecting It with Depends

To use a dependency, add a parameter to your endpoint and set its default to Depends(the_function). FastAPI runs the dependency first, then passes its return value in as that parameter:

from fastapi import FastAPI, Depends

app = FastAPI()

def pagination(skip: int = 0, limit: int = 10):
    return {"skip": skip, "limit": limit}

@app.get("/tasks")
def list_tasks(page: dict = Depends(pagination)):
    return {"endpoint": "tasks", **page}

@app.get("/users")
def list_users(page: dict = Depends(pagination)):
    return {"endpoint": "users", **page}

Both endpoints now get their pagination from the same function. Notice the skip and limit query parameters still work on each endpoint — FastAPI sees them on the dependency and wires them up automatically, even documenting them in /docs:

from fastapi.testclient import TestClient

client = TestClient(app)
print(client.get("/tasks").json())
print(client.get("/tasks?skip=5&limit=2").json())
print(client.get("/users?limit=3").json())
{'endpoint': 'tasks', 'skip': 0, 'limit': 10}
{'endpoint': 'tasks', 'skip': 5, 'limit': 2}
{'endpoint': 'users', 'skip': 0, 'limit': 3}

The defaults applied when nothing was passed; the query parameters flowed through when they were. The pagination logic lives in exactly one function, and both endpoints share it. Add a new list endpoint tomorrow and it’s one line — page: dict = Depends(pagination) — to get the same behavior.

“Injection” just means FastAPI calls it for you

You never call pagination() yourself. You declare the dependency with Depends, and FastAPI resolves it before your endpoint runs, passing the result in — that’s the “injection.” Because the dependency is an ordinary function, you can also call it directly in a test, which makes dependency-based code easy to test in isolation.


Why This Matters Beyond Pagination

Pagination is the gentle introduction, but the real power shows up with weightier shared logic. The same pattern handles:

  • Authentication — a get_current_user dependency that checks a token and returns the user (or raises 401), reused on every protected endpoint.
  • Database sessions — a dependency that opens a connection, hands it to the endpoint, and closes it afterward (you’ll do this with yield next lesson, and for real in Module 5).
  • Shared settings or clients — configuration or an external API client created once and injected where needed.

In every case the win is the same: write it once, declare it where you need it, change it in one place. That’s why dependency injection is the backbone of well-structured FastAPI apps.


Practice Exercises

Exercise 1: Name the win

A teammate has skip and limit copy-pasted into six list endpoints and wants to add a rule capping limit at 100. How does turning it into a dependency help, and how many places would they edit after that?

Hint

Moving skip/limit into one pagination dependency means the cap rule is written once, in that function. After refactoring, adding the cap is a single edit instead of six — and every endpoint stays consistent automatically.

Exercise 2: Predict the output

With the pagination dependency above, what does GET /users?skip=20 return? Which value comes from the request and which from a default?

Hint

It returns {"endpoint": "users", "skip": 20, "limit": 10}. skip is 20 from the query string; limit falls back to its default of 10 because it wasn’t supplied. The dependency’s parameter rules apply exactly as if they were on the endpoint.

Exercise 3: Spot a dependency candidate

Three endpoints each start by reading an X-API-Key header and rejecting the request if it’s wrong. Is this a good candidate for a dependency? Why?

Hint

Yes — it’s shared logic repeated across endpoints, exactly what dependencies are for. A single verify_api_key dependency can do the check (and raise 401 on failure) and be injected wherever it’s needed. You’ll build dependencies like this in the next lesson.


Summary

Dependency injection lets you write shared logic once and reuse it across endpoints instead of copy-pasting. A dependency is just a function that returns what an endpoint needs; you attach it with param = Depends(the_function), and FastAPI runs it before your endpoint and injects the result. You saw a pagination dependency shared by two endpoints — its skip/limit query parameters still worked and were documented, while the logic lived in one place. The same pattern powers authentication, database sessions, and shared configuration, which is why it’s central to structuring real FastAPI apps.

Key Concepts

  • Dependency injection — reusing logic by declaring what an endpoint needs.
  • Dependency — a function (or class) that produces a value for endpoints.
  • Depends(...) — declares a dependency; FastAPI runs it and injects the result.
  • DRY — the shared logic lives in one place, edited once, applied everywhere.

Why This Matters

Dependency injection is what keeps a FastAPI codebase clean as it grows from five endpoints to five hundred. It eliminates duplication, makes behavior consistent across endpoints, and makes code easy to test (you can call dependencies directly or swap them out). Nearly every serious FastAPI app leans on it heavily — for auth, for database access, for configuration — so the pattern you learned here is one you’ll reach for in almost every project.


Next Steps

Continue to Lesson 2 - Reusable and Sub-Dependencies

Use classes as dependencies, build dependencies on top of other dependencies, and manage setup and teardown with yield.

Back to Module Overview

Return to the Structure, Dependencies, and Middleware module overview


Continue Building Your Skills

You can now share logic cleanly with Depends instead of repeating it. Next you’ll go further — dependencies built from classes, dependencies that use other dependencies, and dependencies that set up and tear down resources like database sessions with yield.