Module 1: Dependency Injection
Basic Depends() — Your First Dependency Injection
Capsule overview
Depends() is a FastAPI function that takes another function as an argument and tells FastAPI: "run this function before the endpoint and hand me the result." It's that simple. No magic decorators, no XML configuration, no service registry. A normal Python function + Depends() = dependency injection.
In this capsule you're going to create your first dependency functions: one to encapsulate pagination parameters (skip, limit), another to look up an item by ID with 404 handling, and another for common filters. You'll see how FastAPI automatically resolves your dependencies' parameters — if your dependency takes task_id: int and the path has {task_id}, FastAPI wires them together without you doing anything extra.
By the end of this capsule, every repeated pattern in your API will have a single source of truth.
Anatomy of a dependency
A dependency is a regular Python function. It doesn't inherit from anything, it doesn't need special decorators, and it doesn't need to register anywhere:
from fastapi import Query
def pagination_params(
skip: int = Query(default=0, ge=0),
limit: int = Query(default=10, ge=1, le=100),
) -> dict:
return {"skip": skip, "limit": limit}
This function:
- Takes parameters with type hints (just like an endpoint)
- Can use
Query(),Path(),Header(),Body()— everything an endpoint can - Returns a value that the endpoint will receive
- Is a completely normal Python function — you can test it on its own
To use it as a dependency:
from fastapi import FastAPI, Depends
app = FastAPI()
@app.get("/tasks")
def list_tasks(pagination: dict = Depends(pagination_params)):
skip = pagination["skip"]
limit = pagination["limit"]
return tasks[skip : skip + limit]
Depends(pagination_params) tells FastAPI:
- Before running
list_tasks, runpagination_params - FastAPI reads
pagination_params' signature and pullsskipandlimitfrom the query parameters - The value returned by
pagination_paramsgets assigned topagination
A complete example: pagination as a dependency
Without a dependency (before)
from fastapi import FastAPI, Query
app = FastAPI()
tasks = [
{"id": 1, "title": "Task 1", "completed": False},
{"id": 2, "title": "Task 2", "completed": True},
{"id": 3, "title": "Task 3", "completed": False},
{"id": 4, "title": "Task 4", "completed": True},
{"id": 5, "title": "Task 5", "completed": False},
]
@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/completed")
def list_completed(
skip: int = Query(default=0, ge=0),
limit: int = Query(default=10, ge=1, le=100),
):
completed = [t for t in tasks if t["completed"]]
return completed[skip : skip + limit]
@app.get("/tasks/pending")
def list_pending(
skip: int = Query(default=0, ge=0),
limit: int = Query(default=10, ge=1, le=100),
):
pending = [t for t in tasks if not t["completed"]]
return pending[skip : skip + limit]
skip and limit with their constraints repeat three times. If you want to change the maximum from 100 to 50, you have to find and modify all three endpoints.
With a dependency (after)
from fastapi import FastAPI, Depends, Query
app = FastAPI()
tasks = [
{"id": 1, "title": "Task 1", "completed": False},
{"id": 2, "title": "Task 2", "completed": True},
{"id": 3, "title": "Task 3", "completed": False},
{"id": 4, "title": "Task 4", "completed": True},
{"id": 5, "title": "Task 5", "completed": False},
]
def pagination_params(
skip: int = Query(default=0, ge=0),
limit: int = Query(default=10, ge=1, le=100),
) -> dict:
return {"skip": skip, "limit": limit}
@app.get("/tasks")
def list_tasks(pagination: dict = Depends(pagination_params)):
return tasks[pagination["skip"] : pagination["skip"] + pagination["limit"]]
@app.get("/tasks/completed")
def list_completed(pagination: dict = Depends(pagination_params)):
completed = [t for t in tasks if t["completed"]]
return completed[pagination["skip"] : pagination["skip"] + pagination["limit"]]
@app.get("/tasks/pending")
def list_pending(pagination: dict = Depends(pagination_params)):
pending = [t for t in tasks if not t["completed"]]
return pending[pagination["skip"] : pagination["skip"] + pagination["limit"]]
Now skip and limit are defined in exactly one place. All three endpoints use Depends(pagination_params).
Check it in /docs
Open http://localhost:8000/docs and look at any of the three endpoints. You'll see that skip and limit show up as query parameters with their constraints — FastAPI automatically detects the dependency's parameters and documents them.
A dependency for item lookup with 404
This is the most common use case: look up an item by ID and raise 404 if it doesn't exist.
from fastapi import FastAPI, Depends, HTTPException
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 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)):
task["completed"] = True
return task
@app.delete("/tasks/{task_id}")
def delete_task(task: dict = Depends(get_task_or_404)):
tasks.remove(task)
return {"message": "Task deleted", "task": task}
How does FastAPI know that task_id comes from the path?
When FastAPI sees task: dict = Depends(get_task_or_404) in an endpoint whose path is /tasks/{task_id}, here's what it does:
- It reads
get_task_or_404's signature → it hastask_id: int - It looks for
task_idin the endpoint's path → it finds it in{task_id} - It pulls the value from the path and passes it to
get_task_or_404
You don't need to pass task_id explicitly. FastAPI connects the dependency's parameter with the endpoint's path parameter automatically.
The full flow
Request: GET /tasks/2
↓
FastAPI reads get_task's signature → sees Depends(get_task_or_404)
↓
FastAPI reads get_task_or_404's signature → it needs task_id: int
↓
FastAPI pulls task_id=2 from the path
↓
Runs get_task_or_404(task_id=2)
↓
If task_id=2 exists → returns the task dict
↓
Assigns that dict to "task" in the endpoint
↓
Runs get_task(task={"id": 2, "title": "Study FastAPI", ...})
↓
Response: {"id": 2, "title": "Study FastAPI", ...}
Request: GET /tasks/999
↓
FastAPI runs get_task_or_404(task_id=999)
↓
999 doesn't exist → raise HTTPException(404)
↓
get_task() NEVER runs
↓
Response: {"detail": "Task 999 not found"} (status 404)
A dependency for common filters
If several endpoints share the same filters, you can encapsulate them:
from typing import Optional
from fastapi import FastAPI, 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"},
{"id": 4, "title": "Read a book", "completed": True, "priority": "medium"},
{"id": 5, "title": "Prepare presentation", "completed": False, "priority": "high"},
]
def common_task_filters(
completed: Optional[bool] = None,
priority: Optional[str] = Query(default=None, pattern="^(high|medium|low)$"),
search: Optional[str] = Query(default=None, min_length=1),
) -> dict:
return {"completed": completed, "priority": priority, "search": search}
def pagination_params(
skip: int = Query(default=0, ge=0),
limit: int = Query(default=10, ge=1, le=100),
) -> dict:
return {"skip": skip, "limit": limit}
def apply_filters(items: list, filters: dict) -> list:
result = items
if filters["completed"] is not None:
result = [t for t in result if t["completed"] == filters["completed"]]
if filters["priority"]:
result = [t for t in result if t["priority"] == filters["priority"]]
if filters["search"]:
term = filters["search"].lower()
result = [t for t in result if term in t["title"].lower()]
return result
@app.get("/tasks")
def list_tasks(
filters: dict = Depends(common_task_filters),
pagination: dict = Depends(pagination_params),
):
filtered = apply_filters(tasks, filters)
start = pagination["skip"]
end = start + pagination["limit"]
return {
"total": len(filtered),
"tasks": filtered[start:end],
}
Now the list_tasks endpoint has two dependencies: common_task_filters and pagination_params. FastAPI runs both before the endpoint and passes the results along.
Multiple dependencies in one endpoint
You can have as many dependencies as you need:
@app.get("/tasks")
def list_tasks(
filters: dict = Depends(common_task_filters),
pagination: dict = Depends(pagination_params),
):
...
FastAPI resolves all the dependencies and passes their results to the endpoint. The resolution order doesn't matter as long as the dependencies are independent of each other.
Dependencies that only run (with no return)
Sometimes you need a dependency that checks something but doesn't return a useful value. For example, verifying an API key:
from fastapi import FastAPI, Depends, Header, HTTPException
app = FastAPI()
VALID_API_KEYS = {"key-123", "key-456", "key-789"}
def verify_api_key(x_api_key: str = Header(...)):
if x_api_key not in VALID_API_KEYS:
raise HTTPException(status_code=403, detail="Invalid API key")
@app.get("/tasks", dependencies=[Depends(verify_api_key)])
def list_tasks():
return [{"id": 1, "title": "Task 1"}]
@app.post("/tasks", dependencies=[Depends(verify_api_key)])
def create_task():
return {"message": "created"}
Notice the syntactic difference:
| Form | When to use it |
|---|---|
param: Type = Depends(fn) | When you need the value the dependency returns |
dependencies=[Depends(fn)] | When you only need the dependency to run (verification, logging) |
With dependencies=[Depends(verify_api_key)], the dependency runs (and can raise exceptions), but it doesn't return anything to the endpoint.
A dependency that reaches into the Request
Dependency functions can take the Request object directly:
from fastapi import FastAPI, Depends, Request
app = FastAPI()
def log_request_info(request: Request) -> dict:
return {
"method": request.method,
"url": str(request.url),
"client": request.client.host if request.client else "unknown",
"headers_count": len(request.headers),
}
@app.get("/debug")
def debug_info(info: dict = Depends(log_request_info)):
return info
curl http://127.0.0.1:8000/debug
{"method": "GET", "url": "http://127.0.0.1:8000/debug", "client": "127.0.0.1", "headers_count": 4}
FastAPI detects that request: Request is a special type and injects it automatically — it doesn't come from the query or the body.
A dependency with a Pydantic model as its return
Instead of returning a dict, you can return a Pydantic model to get type safety:
from fastapi import FastAPI, Depends, Query
from pydantic import BaseModel
class PaginationParams(BaseModel):
skip: int
limit: int
def pagination_params(
skip: int = Query(default=0, ge=0),
limit: int = Query(default=10, ge=1, le=100),
) -> PaginationParams:
return PaginationParams(skip=skip, limit=limit)
app = FastAPI()
tasks = [{"id": i, "title": f"Task {i}"} for i in range(1, 21)]
@app.get("/tasks")
def list_tasks(pagination: PaginationParams = Depends(pagination_params)):
return tasks[pagination.skip : pagination.skip + pagination.limit]
With a Pydantic model, your editor knows that pagination.skip is an int — you get autocompletion and type checking.
Dependency caching (use_cache)
By default, if an endpoint uses the same dependency more than once (directly or through sub-dependencies), FastAPI runs it only once per request and reuses the result. This is called dependency caching:
from fastapi import FastAPI, Depends
app = FastAPI()
call_count = 0
def get_settings():
global call_count
call_count += 1
print(f"get_settings called (count: {call_count})")
return {"app_name": "Task API", "version": "1.0"}
def dependency_a(settings: dict = Depends(get_settings)):
return {"a": True, "settings": settings}
def dependency_b(settings: dict = Depends(get_settings)):
return {"b": True, "settings": settings}
@app.get("/test")
def test_caching(
a: dict = Depends(dependency_a),
b: dict = Depends(dependency_b),
):
return {"a": a, "b": b}
Even though dependency_a and dependency_b both depend on get_settings, FastAPI only runs get_settings once per request. Both receive the same result.
If you need it to run every time (with no cache), use Depends(get_settings, use_cache=False).
A complete example app
Here's a full file you can run with uvicorn app.main:app --reload:
from typing import Optional
from fastapi import FastAPI, Depends, HTTPException, Query
from pydantic import BaseModel, Field
app = FastAPI(title="Task API with DI", version="1.0.0")
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"},
{"id": 4, "title": "Read a book", "completed": True, "priority": "medium"},
{"id": 5, "title": "Prepare presentation", "completed": False, "priority": "high"},
]
class TaskCreate(BaseModel):
title: str = Field(min_length=1, max_length=200)
priority: str = Field(default="medium", pattern="^(high|medium|low)$")
class TaskUpdate(BaseModel):
title: Optional[str] = Field(default=None, min_length=1, max_length=200)
completed: Optional[bool] = None
priority: Optional[str] = Field(default=None, pattern="^(high|medium|low)$")
def pagination_params(
skip: int = Query(default=0, ge=0),
limit: int = Query(default=10, ge=1, le=100),
) -> dict:
return {"skip": skip, "limit": limit}
def common_filters(
completed: Optional[bool] = None,
priority: Optional[str] = Query(default=None, pattern="^(high|medium|low)$"),
search: Optional[str] = Query(default=None, min_length=1),
) -> dict:
return {"completed": completed, "priority": priority, "search": search}
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")
def generate_id() -> int:
if not tasks:
return 1
return max(t["id"] for t in tasks) + 1
@app.get("/tasks")
def list_tasks(
filters: dict = Depends(common_filters),
pagination: dict = Depends(pagination_params),
):
result = tasks[:]
if filters["completed"] is not None:
result = [t for t in result if t["completed"] == filters["completed"]]
if filters["priority"]:
result = [t for t in result if t["priority"] == filters["priority"]]
if filters["search"]:
term = filters["search"].lower()
result = [t for t in result if term in t["title"].lower()]
start = pagination["skip"]
end = start + pagination["limit"]
return {"total": len(result), "tasks": result[start:end]}
@app.get("/tasks/{task_id}")
def get_task(task: dict = Depends(get_task_or_404)):
return task
@app.post("/tasks", status_code=201)
def create_task(task_data: TaskCreate):
new_task = task_data.model_dump()
new_task["id"] = generate_id()
new_task["completed"] = False
tasks.append(new_task)
return new_task
@app.patch("/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 {"message": "Task deleted", "task": task}
Try it
# List with pagination
curl "http://127.0.0.1:8000/tasks?skip=0&limit=2"
# Filter by priority
curl "http://127.0.0.1:8000/tasks?priority=high"
# Search by text
curl "http://127.0.0.1:8000/tasks?search=study"
# Get a task (it exists)
curl http://127.0.0.1:8000/tasks/2
# Get a task (it doesn't exist)
curl http://127.0.0.1:8000/tasks/999
# {"detail":"Task 999 not found"}
# Update
curl -X PATCH http://127.0.0.1:8000/tasks/1 \
-H "Content-Type: application/json" \
-d '{"completed": true}'
# Delete
curl -X DELETE http://127.0.0.1:8000/tasks/3
When to use dependencies and when not to
Not every function needs to be a dependency. Use this guide:
| Situation | Dependency? | Why |
|---|---|---|
| Logic repeated in 2+ endpoints | ✅ Yes | DRY — a single source of truth |
| Common parameters (skip, limit) | ✅ Yes | Encapsulates constraints and defaults |
| Item lookup + 404 | ✅ Yes | Removes repetition, sets a precondition |
| Verification (API key, permissions) | ✅ Yes | A precondition that can fail |
| A pure helper (format_date, slugify) | ❌ No | It doesn't need injection — just call it |
| Logic used in a single endpoint | ❌ Probably not | No reuse benefit |
| Calculations with no side effects | ❌ No | A regular function is simpler |
The rule: if the function pulls parameters out of the request, handles HTTP errors, or needs to be replaceable in testing, it's a dependency candidate.
Exercises
Exercise 1: A pagination dependency (Easy)
Create a dependency function pagination that takes page (int, default 1, ge 1) and size (int, default 10, ge 1, le 50) and returns a dict with the computed skip and limit. Use this dependency in a GET /items endpoint that paginates a list of 30 items.
See solution
from fastapi import FastAPI, Depends, Query
app = FastAPI()
items = [{"id": i, "name": f"Item {i}"} for i in range(1, 31)]
def pagination(
page: int = Query(default=1, ge=1),
size: int = Query(default=10, ge=1, le=50),
) -> dict:
skip = (page - 1) * size
return {"skip": skip, "limit": size}
@app.get("/items")
def list_items(pag: dict = Depends(pagination)):
return {
"items": items[pag["skip"] : pag["skip"] + pag["limit"]],
"total": len(items),
}
curl "http://127.0.0.1:8000/items?page=1&size=5"
# Returns items 1-5
curl "http://127.0.0.1:8000/items?page=3&size=5"
# Returns items 11-15
page=3 with size=5 results in skip=10 → items 11 through 15.
Exercise 2: A lookup dependency with 404 (Easy)
Create a list of products (id, name, price, in_stock). Implement get_product_or_404(product_id: int) as a dependency. Use it in GET /products/{product_id}, PATCH /products/{product_id} (toggle in_stock), and DELETE /products/{product_id}.
See solution
from fastapi import FastAPI, Depends, HTTPException
app = FastAPI()
products = [
{"id": 1, "name": "Laptop", "price": 999.99, "in_stock": True},
{"id": 2, "name": "Mouse", "price": 29.99, "in_stock": True},
{"id": 3, "name": "Monitor", "price": 349.99, "in_stock": False},
]
def get_product_or_404(product_id: int) -> dict:
for product in products:
if product["id"] == product_id:
return product
raise HTTPException(status_code=404, detail=f"Product {product_id} not found")
@app.get("/products/{product_id}")
def get_product(product: dict = Depends(get_product_or_404)):
return product
@app.patch("/products/{product_id}")
def toggle_stock(product: dict = Depends(get_product_or_404)):
product["in_stock"] = not product["in_stock"]
return product
@app.delete("/products/{product_id}")
def delete_product(product: dict = Depends(get_product_or_404)):
products.remove(product)
return {"message": f"Product '{product['name']}' deleted"}
curl http://127.0.0.1:8000/products/1
# {"id":1,"name":"Laptop",...}
curl http://127.0.0.1:8000/products/999
# {"detail":"Product 999 not found"}
curl -X PATCH http://127.0.0.1:8000/products/3
# {"id":3,"name":"Monitor","price":349.99,"in_stock":true}
Exercise 3: A common-filters dependency (Medium)
Create a list of books with title, author, genre, year, available. Implement book_filters(genre, author, available, min_year, max_year) as a dependency. Combine it with pagination_params in GET /books.
See solution
from typing import Optional
from fastapi import FastAPI, Depends, Query
app = FastAPI()
books = [
{"id": 1, "title": "One Hundred Years of Solitude", "author": "García Márquez", "genre": "fiction", "year": 1967, "available": True},
{"id": 2, "title": "Don Quixote", "author": "Cervantes", "genre": "fiction", "year": 1605, "available": True},
{"id": 3, "title": "The Little Prince", "author": "Saint-Exupéry", "genre": "fable", "year": 1943, "available": False},
{"id": 4, "title": "Hopscotch", "author": "Cortázar", "genre": "fiction", "year": 1963, "available": True},
{"id": 5, "title": "Cosmos", "author": "Carl Sagan", "genre": "science", "year": 1980, "available": True},
]
def pagination_params(
skip: int = Query(default=0, ge=0),
limit: int = Query(default=10, ge=1, le=100),
) -> dict:
return {"skip": skip, "limit": limit}
def book_filters(
genre: Optional[str] = None,
author: Optional[str] = None,
available: Optional[bool] = None,
min_year: Optional[int] = Query(default=None, ge=1000),
max_year: Optional[int] = Query(default=None, le=2030),
) -> dict:
return {
"genre": genre,
"author": author,
"available": available,
"min_year": min_year,
"max_year": max_year,
}
@app.get("/books")
def list_books(
filters: dict = Depends(book_filters),
pagination: dict = Depends(pagination_params),
):
result = books[:]
if filters["genre"]:
result = [b for b in result if b["genre"] == filters["genre"]]
if filters["author"]:
term = filters["author"].lower()
result = [b for b in result if term in b["author"].lower()]
if filters["available"] is not None:
result = [b for b in result if b["available"] == filters["available"]]
if filters["min_year"] is not None:
result = [b for b in result if b["year"] >= filters["min_year"]]
if filters["max_year"] is not None:
result = [b for b in result if b["year"] <= filters["max_year"]]
total = len(result)
start = pagination["skip"]
end = start + pagination["limit"]
return {"total": total, "books": result[start:end]}
curl "http://127.0.0.1:8000/books?genre=fiction"
# Returns 3 fiction books
curl "http://127.0.0.1:8000/books?min_year=1960&available=true"
# Returns available books published from 1960 onward
curl "http://127.0.0.1:8000/books?author=garc"
# Partial search by author
Exercise 4: A dependency with Header (Medium)
Create a verify_api_key dependency that reads the X-API-Key header and validates that it's in a list of valid keys. Use dependencies=[Depends(verify_api_key)] on a POST endpoint. Try it with and without the header.
See solution
from fastapi import FastAPI, Depends, Header, HTTPException
app = FastAPI()
VALID_KEYS = {"secret-key-1", "secret-key-2", "dev-key"}
def verify_api_key(x_api_key: str = Header(...)):
if x_api_key not in VALID_KEYS:
raise HTTPException(
status_code=403,
detail="Invalid or missing API key",
)
@app.get("/public")
def public_endpoint():
return {"message": "Open access"}
@app.post("/tasks", dependencies=[Depends(verify_api_key)], status_code=201)
def create_task():
return {"message": "Task created (protected)"}
@app.delete("/tasks/1", dependencies=[Depends(verify_api_key)])
def delete_task():
return {"message": "Task deleted (protected)"}
# Without an API key → 422 (header required)
curl -X POST http://127.0.0.1:8000/tasks
# {"detail":[{"type":"missing","loc":["header","x-api-key"],...}]}
# With an invalid key → 403
curl -X POST http://127.0.0.1:8000/tasks -H "X-API-Key: wrong"
# {"detail":"Invalid or missing API key"}
# With a valid key → 201
curl -X POST http://127.0.0.1:8000/tasks -H "X-API-Key: secret-key-1"
# {"message":"Task created (protected)"}
# Public endpoint → no restriction
curl http://127.0.0.1:8000/public
# {"message":"Open access"}
Exercise 5: A dependency with Request (Medium-Hard)
Create an extract_client_info dependency that reaches into the Request object and returns a dict with method, path, client_ip, and user_agent. Use this dependency in two different endpoints and check that the info changes with each request.
See solution
from fastapi import FastAPI, Depends, Request
app = FastAPI()
def extract_client_info(request: Request) -> dict:
return {
"method": request.method,
"path": str(request.url.path),
"client_ip": request.client.host if request.client else "unknown",
"user_agent": request.headers.get("user-agent", "unknown"),
}
@app.get("/info")
def get_info(client: dict = Depends(extract_client_info)):
return {"endpoint": "info", "client": client}
@app.post("/echo")
def echo(client: dict = Depends(extract_client_info)):
return {"endpoint": "echo", "client": client}
curl http://127.0.0.1:8000/info
# {"endpoint":"info","client":{"method":"GET","path":"/info","client_ip":"127.0.0.1",...}}
curl -X POST http://127.0.0.1:8000/echo
# {"endpoint":"echo","client":{"method":"POST","path":"/echo","client_ip":"127.0.0.1",...}}
method and path change depending on which endpoint you call.
Exercise 6: Combining multiple dependencies (Hard)
Create a notes API with: pagination_params, get_note_or_404, and note_filters(category, search). Implement GET /notes (uses filters + pagination), GET /notes/{note_id} (uses lookup), PUT /notes/{note_id} (uses lookup + body), and DELETE /notes/{note_id} (uses lookup). Include at least 5 initial notes.
See solution
from typing import Optional
from fastapi import FastAPI, Depends, HTTPException, Query
from pydantic import BaseModel, Field
app = FastAPI(title="Notes API with DI")
notes = [
{"id": 1, "title": "Project ideas", "content": "Design a REST API...", "category": "work"},
{"id": 2, "title": "Shopping list", "content": "Milk, bread, eggs", "category": "personal"},
{"id": 3, "title": "Bug tracker", "content": "Fix login flow", "category": "work"},
{"id": 4, "title": "Pasta recipe", "content": "Boil water...", "category": "personal"},
{"id": 5, "title": "Sprint planning", "content": "Prioritize the backlog", "category": "work"},
]
class NoteUpdate(BaseModel):
title: Optional[str] = Field(default=None, min_length=1, max_length=200)
content: Optional[str] = Field(default=None, min_length=1)
category: Optional[str] = None
def pagination_params(
skip: int = Query(default=0, ge=0),
limit: int = Query(default=10, ge=1, le=50),
) -> dict:
return {"skip": skip, "limit": limit}
def note_filters(
category: Optional[str] = None,
search: Optional[str] = Query(default=None, min_length=1),
) -> dict:
return {"category": category, "search": search}
def get_note_or_404(note_id: int) -> dict:
for note in notes:
if note["id"] == note_id:
return note
raise HTTPException(status_code=404, detail=f"Note {note_id} not found")
@app.get("/notes")
def list_notes(
filters: dict = Depends(note_filters),
pagination: dict = Depends(pagination_params),
):
result = notes[:]
if filters["category"]:
result = [n for n in result if n["category"] == filters["category"]]
if filters["search"]:
term = filters["search"].lower()
result = [n for n in result if term in n["title"].lower() or term in n["content"].lower()]
total = len(result)
start = pagination["skip"]
return {"total": total, "notes": result[start : start + pagination["limit"]]}
@app.get("/notes/{note_id}")
def get_note(note: dict = Depends(get_note_or_404)):
return note
@app.put("/notes/{note_id}")
def update_note(note: dict = Depends(get_note_or_404), data: NoteUpdate = ...):
update = data.model_dump(exclude_unset=True)
note.update(update)
return note
@app.delete("/notes/{note_id}")
def delete_note(note: dict = Depends(get_note_or_404)):
notes.remove(note)
return {"message": f"Note '{note['title']}' deleted"}
curl "http://127.0.0.1:8000/notes?category=work"
# {"total":3,"notes":[...work notes...]}
curl "http://127.0.0.1:8000/notes?search=list"
# Searches in title and content
curl http://127.0.0.1:8000/notes/1
# Returns note 1
curl -X PUT http://127.0.0.1:8000/notes/1 \
-H "Content-Type: application/json" \
-d '{"title": "Project ideas v2"}'
# Updates only the title
curl -X DELETE http://127.0.0.1:8000/notes/2
# {"message":"Note 'Shopping list' deleted"}
Troubleshooting
Problem 1: "depends" isn't recognized as an import
Cause: You're importing depends in lowercase, or from the wrong module.
# ❌ Wrong
from fastapi import depends
from fastapi.dependencies import Depends
# ✅ Correct
from fastapi import Depends
Problem 2: The dependency doesn't receive the path parameter
Cause: The parameter's name in the dependency doesn't match the name in the path.
# ❌ The path has {task_id} but the dependency takes id
@app.get("/tasks/{task_id}")
def get_task(task: dict = Depends(get_task_or_404)):
...
def get_task_or_404(id: int): # ← "id" doesn't match "task_id"
...
# ✅ The names have to match
def get_task_or_404(task_id: int): # ← matches {task_id}
...
Problem 3: 422 "field required" when using a dependency with Header
Cause: Header(...) expects the header but the client isn't sending it. FastAPI converts the parameter's name: x_api_key looks for the x-api-key header (underscores → hyphens).
# ❌ Wrong header
curl -H "X_API_Key: abc" http://127.0.0.1:8000/tasks
# ✅ Correct header (with hyphens)
curl -H "X-API-Key: abc" http://127.0.0.1:8000/tasks
Problem 4: The dependency runs but the endpoint doesn't receive the value
Cause: You're using dependencies=[Depends(fn)] instead of param = Depends(fn).
# ❌ With dependencies=[] the value isn't passed to the endpoint
@app.get("/tasks", dependencies=[Depends(pagination_params)])
def list_tasks():
# pagination_params ran but I don't have the result
...
# ✅ With param = Depends() the value gets assigned to the parameter
@app.get("/tasks")
def list_tasks(pagination: dict = Depends(pagination_params)):
# pagination holds the result
...
Problem 5: A dependency with Query() returns a strange validation error
Cause: A conflict between an endpoint parameter and a dependency parameter with the same name.
# ❌ Both define "limit"
def pagination(limit: int = Query(default=10)): ...
@app.get("/tasks")
def list_tasks(limit: int = 5, pag: dict = Depends(pagination)):
...
# ✅ Define it in only one place (the dependency)
@app.get("/tasks")
def list_tasks(pag: dict = Depends(pagination)):
...
Summary
Depends(fn)injects the result offnas an endpoint parameter- A dependency function is a normal Python function with type hints
- FastAPI automatically resolves the dependencies' path params, query params, and headers
param = Depends(fn)when you need the value;dependencies=[Depends(fn)]when you're only verifying- If the dependency raises
HTTPException, the endpoint doesn't run - Multiple dependencies in one endpoint: they all get resolved before it runs
- Dependencies are cached per request by default —
use_cache=Falseto turn that off - You can return a
dict, a Pydantic model, or any type from a dependency - Dependencies can reach into
Request,Header,Query,Path— just like endpoints
Additional resources
- FastAPI - First Steps with Dependencies — The official step-by-step tutorial
- FastAPI - Dependencies in path operation decorators — The dependencies=[] syntax
- FastAPI - Request object — Reaching into the Request from endpoints and dependencies
- FastAPI - Header Parameters — Headers in endpoints and dependencies
- Pydantic v2 - Models — Using Pydantic models as a dependency's return value
What's next?
Next capsule: Sub-dependencies and Composition — What happens when one dependency needs the result of another dependency? You'll build dependency chains, explore class-based dependencies with __call__, and see how to compose small pieces into powerful abstractions.