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.
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_userdependency that checks a token and returns the user (or raises401), 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
yieldnext 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.