Module 1: Dependency Injection

Project: Refactoring the To-Do API with Dependency Injection

Project overview

In this project you take your To-Do API from FastAPI Fundamentals — with its CRUD endpoints, Pydantic models, and error handling — and refactor it by applying Dependency Injection. You aren't changing what the API does. You're changing how it does it. The repeated logic gets extracted into reusable dependencies. Data access gets centralized. Pagination gets encapsulated. The task lookup with its 404 gets written exactly once.

The result is an API with exactly the same endpoints and responses, but with significantly cleaner code. Each endpoint focuses on its business logic — the preconditions (the task exists, pagination is parsed, the data is reachable) are handled by the dependencies.

You'll pull together everything from capsules 02–04: basic Depends() for pagination and filters, sub-dependencies for composition, class-based dependencies for configuration, yield dependencies for the data store, and dependency_overrides to confirm that testing works.


Project objectives

By the end of this project:

  • ✅ You extract pagination (skip, limit) into a reusable dependency function
  • ✅ You extract the task lookup + 404 into get_task_or_404
  • ✅ You centralize data access with a get_task_store yield dependency
  • ✅ You create a Paginator class-based dependency with a configurable limit
  • ✅ You use sub-dependencies: get_task_or_404 depends on get_task_store
  • ✅ You implement common filters as a dependency
  • ✅ You use dependency_overrides to confirm you can replace the data store in tests
  • ✅ Your endpoints are shorter and focused on business logic
  • ✅ The API behaves exactly as it did before the refactor

Why this project?

Look at these three endpoints from an API without DI:

@app.get("/tasks/{task_id}")
def get_task(task_id: int):
    for task in tasks:                    # ← repeated
        if task["id"] == task_id:         # ← repeated
            return task                   # ← repeated
    raise HTTPException(404, ...)         # ← repeated

@app.put("/tasks/{task_id}")
def update_task(task_id: int, data: TaskUpdate):
    for task in tasks:                    # ← repeated
        if task["id"] == task_id:         # ← repeated
            task.update(...)
            return task
    raise HTTPException(404, ...)         # ← repeated

@app.delete("/tasks/{task_id}")
def delete_task(task_id: int):
    for i, task in enumerate(tasks):      # ← repeated
        if task["id"] == task_id:         # ← repeated
            return tasks.pop(i)
    raise HTTPException(404, ...)         # ← repeated

That's 4 lines of lookup repeated 3 times. With DI, they get written once:

Without DI:                          With DI (this project):
──────────                           ──────────────────────
3x inline lookup + 404           →   1x get_task_or_404 + Depends()
3x skip/limit in the signature   →   1x pagination_params + Depends()
direct access to a global list   →   1x get_task_store (yield dep)
business logic + plumbing        →   endpoints with business logic only

Technical specifications

Stack

  • Framework: FastAPI
  • Validation: Pydantic v2
  • Server: uvicorn with hot reload
  • Storage: In-memory list (accessed through a yield dependency)

Data model

FieldTypeDescription
idintUnique identifier (auto-generated)
titlestrThe task's title (1-200 chars)
descriptionstr | NoneOptional description
completedboolCompletion state (default False)
prioritystrPriority: high, medium, low (default "medium")
created_atstrCreation date (ISO format)

Endpoints

MethodPathDescriptionDependencies
GET/Service info
GET/tasksList with filters and paginationpagination_params, task_filters, get_task_store
GET/tasks/statsStatisticstask_filters, get_task_store
GET/tasks/{task_id}Get by IDget_task_or_404
POST/tasksCreate a taskget_task_store
PATCH/tasks/{task_id}Partial updateget_task_or_404
DELETE/tasks/{task_id}Deleteget_task_or_404

Dependencies to create

DependencyTypePurpose
get_task_storeYieldReturns the task list (centralizes data access)
pagination_paramsRegularEncapsulates skip and limit with their constraints
task_filtersRegularEncapsulates the filters: completed, priority, search
get_task_or_404Regular (sub-dep)Looks up a task by ID, raises 404 if it doesn't exist
PaginatorClass-basedA configurable paginator with max_limit

Step-by-step guide

Step 1: Create the project structure

fastapi-advanced/
├── app/
│   ├── __init__.py
│   └── main.py       ← all the code (for now)
├── venv/
└── requirements.txt
mkdir -p fastapi-advanced/app
cd fastapi-advanced
python -m venv venv
source venv/bin/activate
pip install "fastapi[standard]"
touch app/__init__.py

Step 2: Define the Pydantic models

Create the models in app/main.py:

from typing import Optional
from datetime import datetime
from pydantic import BaseModel, Field


class TaskCreate(BaseModel):
    title: str = Field(min_length=1, max_length=200, description="The task's title")
    description: Optional[str] = Field(default=None, max_length=1000)
    priority: str = Field(default="medium", pattern="^(high|medium|low)$")

    model_config = {
        "json_schema_extra": {
            "examples": [
                {
                    "title": "Study advanced FastAPI",
                    "description": "Finish the Dependency Injection module",
                    "priority": "high",
                }
            ]
        }
    }


class TaskUpdate(BaseModel):
    title: Optional[str] = Field(default=None, min_length=1, max_length=200)
    description: Optional[str] = Field(default=None, max_length=1000)
    completed: Optional[bool] = None
    priority: Optional[str] = Field(default=None, pattern="^(high|medium|low)$")

Step 3: Create the initial data and the yield dependency for the data store

from fastapi import FastAPI, Depends, HTTPException, Query

initial_tasks = [
    {
        "id": 1,
        "title": "Buy groceries",
        "description": "Milk, bread, eggs, fruit",
        "completed": False,
        "priority": "medium",
        "created_at": "2026-03-10T09:00:00",
    },
    {
        "id": 2,
        "title": "Study Dependency Injection",
        "description": "Finish capsules 02 through 04 of module 1",
        "completed": False,
        "priority": "high",
        "created_at": "2026-03-10T10:30:00",
    },
    {
        "id": 3,
        "title": "Work out",
        "description": None,
        "completed": True,
        "priority": "low",
        "created_at": "2026-03-10T07:00:00",
    },
    {
        "id": 4,
        "title": "Review pull requests",
        "description": "PR #42 and PR #43 pending review",
        "completed": False,
        "priority": "high",
        "created_at": "2026-03-11T14:00:00",
    },
    {
        "id": 5,
        "title": "Prepare presentation",
        "description": "Slides for Friday's demo",
        "completed": True,
        "priority": "medium",
        "created_at": "2026-03-09T16:00:00",
    },
]

task_store = initial_tasks[:]


def get_task_store():
    yield task_store

The get_task_store yield dependency centralizes data access. Today it returns an in-memory list. In the future, it could return a database session — and the endpoints wouldn't change.

Step 4: Create the pagination dependency

class PaginationResult(BaseModel):
    skip: int
    limit: int


def pagination_params(
    skip: int = Query(default=0, ge=0, description="Records to skip"),
    limit: int = Query(default=10, ge=1, le=100, description="Maximum records"),
) -> PaginationResult:
    return PaginationResult(skip=skip, limit=limit)

Use a Pydantic model for the result — you get autocompletion with pagination.skip and pagination.limit.

Step 5: Create the filters dependency

class TaskFilters(BaseModel):
    completed: Optional[bool]
    priority: Optional[str]
    search: Optional[str]


def task_filters(
    completed: Optional[bool] = Query(default=None, description="Filter by state"),
    priority: Optional[str] = Query(
        default=None, pattern="^(high|medium|low)$", description="Filter by priority"
    ),
    search: Optional[str] = Query(
        default=None, min_length=1, description="Search in title and description"
    ),
) -> TaskFilters:
    return TaskFilters(completed=completed, priority=priority, search=search)

Step 6: Create the lookup dependency with a sub-dependency

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

get_task_or_404 uses Depends(get_task_store) — it's a sub-dependency. The store comes from the yield dependency, not from a global variable.

Step 7: Create a class-based dependency (Paginator)

class Paginator:
    def __init__(self, max_limit: int = 100):
        self.max_limit = max_limit

    def __call__(
        self,
        page: int = Query(default=1, ge=1, description="Page number"),
        size: int = Query(default=10, ge=1, description="Page size"),
    ) -> PaginationResult:
        effective_size = min(size, self.max_limit)
        skip = (page - 1) * effective_size
        return PaginationResult(skip=skip, limit=effective_size)


paginator = Paginator(max_limit=50)

Step 8: A helper function to apply the filters

def apply_filters(tasks_list: list, filters: TaskFilters) -> list:
    result = tasks_list[:]
    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()
            or (t["description"] and term in t["description"].lower())
        ]
    return result

This is a helper, not a dependency — it doesn't pull parameters out of the request, it just operates on data.

Step 9: Implement the endpoints

app = FastAPI(
    title="To-Do API with Dependency Injection",
    description="A task API refactored with DI. Module 1 — FastAPI Advanced Features.",
    version="2.0.0",
)


@app.get("/", tags=["General"])
def root():
    return {
        "service": "To-Do API",
        "version": "2.0.0",
        "module": "Dependency Injection",
        "docs": "/docs",
    }


@app.get("/tasks", tags=["Tasks"])
def list_tasks(
    store: list = Depends(get_task_store),
    filters: TaskFilters = Depends(task_filters),
    pagination: PaginationResult = Depends(pagination_params),
):
    filtered = apply_filters(store, filters)
    start = pagination.skip
    end = start + pagination.limit
    return {
        "total": len(filtered),
        "skip": pagination.skip,
        "limit": pagination.limit,
        "tasks": filtered[start:end],
    }


@app.get("/tasks/stats", tags=["Tasks"])
def task_stats(
    store: list = Depends(get_task_store),
    filters: TaskFilters = Depends(task_filters),
):
    filtered = apply_filters(store, filters)
    completed = sum(1 for t in filtered if t["completed"])
    by_priority = {}
    for task in filtered:
        p = task["priority"]
        by_priority[p] = by_priority.get(p, 0) + 1
    return {
        "total": len(filtered),
        "completed": completed,
        "pending": len(filtered) - completed,
        "by_priority": by_priority,
    }


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


@app.post("/tasks", status_code=201, tags=["Tasks"])
def create_task(
    task_data: TaskCreate,
    store: list = Depends(get_task_store),
):
    new_id = max((t["id"] for t in store), default=0) + 1
    new_task = task_data.model_dump()
    new_task["id"] = new_id
    new_task["completed"] = False
    new_task["created_at"] = datetime.now().isoformat()
    store.append(new_task)
    return new_task


@app.patch("/tasks/{task_id}", tags=["Tasks"])
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}", tags=["Tasks"])
def delete_task(
    task: dict = Depends(get_task_or_404),
    store: list = Depends(get_task_store),
):
    store.remove(task)
    return {"message": f"Task '{task['title']}' deleted", "task": task}

Step 10: Check that everything works

uvicorn app.main:app --reload

Try it in /docs or with curl:

# List them all
curl http://127.0.0.1:8000/tasks

# Filter by priority
curl "http://127.0.0.1:8000/tasks?priority=high"

# Search
curl "http://127.0.0.1:8000/tasks?search=study"

# Pagination
curl "http://127.0.0.1:8000/tasks?skip=2&limit=2"

# Statistics
curl http://127.0.0.1:8000/tasks/stats

# Filtered statistics
curl "http://127.0.0.1:8000/tasks/stats?priority=high"

# Get by ID
curl http://127.0.0.1:8000/tasks/1

# Get one that doesn't exist
curl http://127.0.0.1:8000/tasks/999

# Create
curl -X POST http://127.0.0.1:8000/tasks \
  -H "Content-Type: application/json" \
  -d '{"title": "New task with DI", "priority": "high"}'

# 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

The complete project

Structure

fastapi-advanced/
├── app/
│   ├── __init__.py
│   └── main.py       ← all the code
├── venv/
└── requirements.txt

Full code — app/main.py

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


# --- Models ---

class TaskCreate(BaseModel):
    title: str = Field(min_length=1, max_length=200, description="The task's title")
    description: Optional[str] = Field(default=None, max_length=1000)
    priority: str = Field(default="medium", pattern="^(high|medium|low)$")

    model_config = {
        "json_schema_extra": {
            "examples": [
                {
                    "title": "Study advanced FastAPI",
                    "description": "Finish the Dependency Injection module",
                    "priority": "high",
                }
            ]
        }
    }


class TaskUpdate(BaseModel):
    title: Optional[str] = Field(default=None, min_length=1, max_length=200)
    description: Optional[str] = Field(default=None, max_length=1000)
    completed: Optional[bool] = None
    priority: Optional[str] = Field(default=None, pattern="^(high|medium|low)$")


class PaginationResult(BaseModel):
    skip: int
    limit: int


class TaskFilters(BaseModel):
    completed: Optional[bool]
    priority: Optional[str]
    search: Optional[str]


# --- Data Store ---

initial_tasks = [
    {
        "id": 1,
        "title": "Buy groceries",
        "description": "Milk, bread, eggs, fruit",
        "completed": False,
        "priority": "medium",
        "created_at": "2026-03-10T09:00:00",
    },
    {
        "id": 2,
        "title": "Study Dependency Injection",
        "description": "Finish capsules 02 through 04 of module 1",
        "completed": False,
        "priority": "high",
        "created_at": "2026-03-10T10:30:00",
    },
    {
        "id": 3,
        "title": "Work out",
        "description": None,
        "completed": True,
        "priority": "low",
        "created_at": "2026-03-10T07:00:00",
    },
    {
        "id": 4,
        "title": "Review pull requests",
        "description": "PR #42 and PR #43 pending review",
        "completed": False,
        "priority": "high",
        "created_at": "2026-03-11T14:00:00",
    },
    {
        "id": 5,
        "title": "Prepare presentation",
        "description": "Slides for Friday's demo",
        "completed": True,
        "priority": "medium",
        "created_at": "2026-03-09T16:00:00",
    },
]

task_store = initial_tasks[:]


# --- Dependencies ---

def get_task_store():
    yield task_store


def pagination_params(
    skip: int = Query(default=0, ge=0, description="Records to skip"),
    limit: int = Query(default=10, ge=1, le=100, description="Maximum records"),
) -> PaginationResult:
    return PaginationResult(skip=skip, limit=limit)


def task_filters(
    completed: Optional[bool] = Query(default=None, description="Filter by state"),
    priority: Optional[str] = Query(
        default=None, pattern="^(high|medium|low)$", description="Filter by priority"
    ),
    search: Optional[str] = Query(
        default=None, min_length=1, description="Search in title and description"
    ),
) -> TaskFilters:
    return TaskFilters(completed=completed, priority=priority, search=search)


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


class Paginator:
    def __init__(self, max_limit: int = 100):
        self.max_limit = max_limit

    def __call__(
        self,
        page: int = Query(default=1, ge=1, description="Page number"),
        size: int = Query(default=10, ge=1, description="Page size"),
    ) -> PaginationResult:
        effective_size = min(size, self.max_limit)
        skip = (page - 1) * effective_size
        return PaginationResult(skip=skip, limit=effective_size)


paginator = Paginator(max_limit=50)


# --- Helpers ---

def apply_filters(tasks_list: list, filters: TaskFilters) -> list:
    result = tasks_list[:]
    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()
            or (t["description"] and term in t["description"].lower())
        ]
    return result


# --- App ---

app = FastAPI(
    title="To-Do API with Dependency Injection",
    description="A task API refactored with DI. Module 1 — FastAPI Advanced Features.",
    version="2.0.0",
)


# --- Endpoints ---

@app.get("/", tags=["General"])
def root():
    return {
        "service": "To-Do API",
        "version": "2.0.0",
        "module": "Dependency Injection",
        "docs": "/docs",
    }


@app.get("/tasks", tags=["Tasks"])
def list_tasks(
    store: list = Depends(get_task_store),
    filters: TaskFilters = Depends(task_filters),
    pagination: PaginationResult = Depends(pagination_params),
):
    filtered = apply_filters(store, filters)
    start = pagination.skip
    end = start + pagination.limit
    return {
        "total": len(filtered),
        "skip": pagination.skip,
        "limit": pagination.limit,
        "tasks": filtered[start:end],
    }


@app.get("/tasks/stats", tags=["Tasks"])
def task_stats(
    store: list = Depends(get_task_store),
    filters: TaskFilters = Depends(task_filters),
):
    filtered = apply_filters(store, filters)
    completed_count = sum(1 for t in filtered if t["completed"])
    by_priority = {}
    for task in filtered:
        p = task["priority"]
        by_priority[p] = by_priority.get(p, 0) + 1
    return {
        "total": len(filtered),
        "completed": completed_count,
        "pending": len(filtered) - completed_count,
        "by_priority": by_priority,
    }


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


@app.post("/tasks", status_code=201, tags=["Tasks"])
def create_task(
    task_data: TaskCreate,
    store: list = Depends(get_task_store),
):
    new_id = max((t["id"] for t in store), default=0) + 1
    new_task = task_data.model_dump()
    new_task["id"] = new_id
    new_task["completed"] = False
    new_task["created_at"] = datetime.now().isoformat()
    store.append(new_task)
    return new_task


@app.patch("/tasks/{task_id}", tags=["Tasks"])
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}", tags=["Tasks"])
def delete_task(
    task: dict = Depends(get_task_or_404),
    store: list = Depends(get_task_store),
):
    store.remove(task)
    return {"message": f"Task '{task['title']}' deleted", "task": task}

Step-by-step verification

1. Check that the server starts

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

Open http://localhost:8000/docs. You should see the documentation titled "To-Do API with Dependency Injection", with every endpoint organized by tags.

2. Check the listing with pagination

curl -s "http://127.0.0.1:8000/tasks?skip=0&limit=2" | python -m json.tool
{
    "total": 5,
    "skip": 0,
    "limit": 2,
    "tasks": [
        {"id": 1, "title": "Buy groceries", ...},
        {"id": 2, "title": "Study Dependency Injection", ...}
    ]
}

3. Check the filters

# Completed only
curl -s "http://127.0.0.1:8000/tasks?completed=true" | python -m json.tool

# High priority only
curl -s "http://127.0.0.1:8000/tasks?priority=high" | python -m json.tool

# Text search
curl -s "http://127.0.0.1:8000/tasks?search=study" | python -m json.tool

# Combining filters
curl -s "http://127.0.0.1:8000/tasks?completed=false&priority=high" | python -m json.tool

4. Check the statistics

curl -s http://127.0.0.1:8000/tasks/stats | python -m json.tool
{
    "total": 5,
    "completed": 2,
    "pending": 3,
    "by_priority": {"medium": 2, "high": 2, "low": 1}
}

5. Check the lookup + 404

# It exists
curl -s http://127.0.0.1:8000/tasks/1 | python -m json.tool

# It doesn't exist
curl -s http://127.0.0.1:8000/tasks/999 | python -m json.tool
# {"detail": "Task 999 not found"}

6. Check the CRUD

# Create
curl -s -X POST http://127.0.0.1:8000/tasks \
  -H "Content-Type: application/json" \
  -d '{"title": "Task created with DI", "priority": "high"}' | python -m json.tool

# Partial update
curl -s -X PATCH http://127.0.0.1:8000/tasks/1 \
  -H "Content-Type: application/json" \
  -d '{"completed": true}' | python -m json.tool

# Delete
curl -s -X DELETE http://127.0.0.1:8000/tasks/3 | python -m json.tool

7. Check the Pydantic validation

# Empty title
curl -s -X POST http://127.0.0.1:8000/tasks \
  -H "Content-Type: application/json" \
  -d '{"title": ""}' | python -m json.tool

# Invalid priority
curl -s -X POST http://127.0.0.1:8000/tasks \
  -H "Content-Type: application/json" \
  -d '{"title": "Test", "priority": "urgent"}' | python -m json.tool

8. Check dependency_overrides (optional)

Create a test_di.py file at the project root:

from fastapi.testclient import TestClient
from app.main import app, get_task_store

test_tasks = [
    {"id": 100, "title": "Test Task", "description": None,
     "completed": False, "priority": "high", "created_at": "2026-01-01T00:00:00"},
]


def fake_store():
    yield test_tasks


app.dependency_overrides[get_task_store] = fake_store
client = TestClient(app)

response = client.get("/tasks")
data = response.json()
assert data["total"] == 1
assert data["tasks"][0]["id"] == 100
print(f"✅ Override works: {data['total']} test task(s)")

response = client.get("/tasks/100")
assert response.status_code == 200
print(f"✅ Lookup works with the test data")

response = client.get("/tasks/1")
assert response.status_code == 404
print(f"✅ 404 works with the test data (task 1 doesn't exist in test_tasks)")

app.dependency_overrides.clear()
print("\n✅ All the DI tests passed!")
python test_di.py
# ✅ Override works: 1 test task(s)
# ✅ Lookup works with the test data
# ✅ 404 works with the test data (task 1 doesn't exist in test_tasks)
# ✅ All the DI tests passed!

Comparison: before vs after

The GET /tasks/{task_id} endpoint

Before (without DI):

@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")

After (with DI):

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

Reduction: From 5 lines to 1 line of logic. The lookup and the 404 are handled in the dependency.

The DELETE /tasks/{task_id} endpoint

Before:

@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")

After:

@app.delete("/tasks/{task_id}")
def delete_task(task: dict = Depends(get_task_or_404), store: list = Depends(get_task_store)):
    store.remove(task)
    return {"message": f"Task '{task['title']}' deleted", "task": task}

Benefit: The endpoint only does its job (delete). The lookup and the existence check are the dependency's responsibility.

The GET /tasks endpoint with filters

Before:

@app.get("/tasks")
def list_tasks(
    skip: int = Query(default=0, ge=0),
    limit: int = Query(default=10, ge=1, le=100),
    completed: Optional[bool] = None,
    priority: Optional[str] = Query(default=None, pattern="^(high|medium|low)$"),
    search: Optional[str] = Query(default=None, min_length=1),
):
    result = tasks[:]
    # ... 15 lines of filters and pagination

After:

@app.get("/tasks")
def list_tasks(
    store: list = Depends(get_task_store),
    filters: TaskFilters = Depends(task_filters),
    pagination: PaginationResult = Depends(pagination_params),
):
    filtered = apply_filters(store, filters)
    start = pagination.skip
    end = start + pagination.limit
    return {"total": len(filtered), "skip": pagination.skip, "limit": pagination.limit, "tasks": filtered[start:end]}

Benefit: The pagination and filter parameters get defined once. If you add a /tasks/export endpoint that needs the same filters, you just add Depends(task_filters).


Troubleshooting

Problem 1: "Task already removed" or an error when deleting

Cause: get_task_or_404 returns a reference to the dict inside the list. If you try store.remove(task) and the dict was already modified or isn't in the list, it fails.

Fix: Make sure get_task_or_404 searches the same store you use to delete. Both have to use Depends(get_task_store).

Problem 2: The filters don't apply on /tasks/stats

Cause: You forgot to add Depends(task_filters) to the stats endpoint.

# ❌ Without filters
@app.get("/tasks/stats")
def task_stats(store: list = Depends(get_task_store)):
    ...

# ✅ With filters
@app.get("/tasks/stats")
def task_stats(store: list = Depends(get_task_store), filters: TaskFilters = Depends(task_filters)):
    filtered = apply_filters(store, filters)
    ...

Problem 3: /tasks/stats returns 404 instead of statistics

Cause: FastAPI matches /tasks/stats against /tasks/{task_id} and treats "stats" as a task_id. The order of the endpoints matters.

Fix: Define /tasks/stats before /tasks/{task_id}:

@app.get("/tasks/stats")  # ← fixed paths first
def task_stats(...): ...

@app.get("/tasks/{task_id}")  # ← parameterized paths after
def get_task(...): ...

Problem 4: dependency_overrides doesn't affect the endpoint

Cause: The override's key doesn't match the original function. Make sure you import exactly the same function.

# ❌ You import a copy or a different function
from app.main import get_task_store as store_fn
app.dependency_overrides[store_fn] = fake  # might not match

# ✅ Import the exact reference
from app.main import get_task_store
app.dependency_overrides[get_task_store] = fake

Problem 5: Data persisting between test requests

Cause: task_store is a mutable list that gets modified in place. If one test creates or deletes tasks, the changes persist into the next test.

Fix: Each test should use its own copy of the data via an override:

def fake_store():
    test_data = [{"id": 1, "title": "Test"}]  # fresh data every time
    yield test_data

Problem 6: search looks in description but fails when it's None

Cause: If description is None, term in None.lower() raises an error.

Fix: Check that description isn't None before searching:

if filters.search:
    term = filters.search.lower()
    result = [
        t for t in result
        if term in t["title"].lower()
        or (t["description"] and term in t["description"].lower())
    ]

Problem 7: Creating a task with max() fails on an empty list

Cause: max(t["id"] for t in store) fails if store is empty.

Fix: Use default=0:

new_id = max((t["id"] for t in store), default=0) + 1

Problem 8: The update endpoint doesn't preserve the fields that weren't sent

Cause: You aren't using exclude_unset=True in model_dump().

# ❌ Overwrites everything
update = task_data.model_dump()

# ✅ Only the fields the client sent
update = task_data.model_dump(exclude_unset=True)

Completion checklist

Before you consider the project finished, check that:

  • The server starts without errors using uvicorn app.main:app --reload
  • /docs shows every endpoint with the "General" and "Tasks" tags
  • GET /tasks returns a list with total, skip, limit, tasks
  • GET /tasks?completed=true filters to completed tasks only
  • GET /tasks?priority=high filters to high priority only
  • GET /tasks?search=study searches in the title and description
  • GET /tasks?skip=2&limit=2 paginates correctly
  • GET /tasks/stats returns total, completed, pending, by_priority
  • GET /tasks/stats?priority=high applies the filters to the statistics
  • GET /tasks/1 returns the task
  • GET /tasks/999 returns 404 with a descriptive message
  • POST /tasks creates a task with Pydantic validation
  • POST /tasks with an empty title returns 422
  • POST /tasks with an invalid priority returns 422
  • PATCH /tasks/1 updates only the fields that were sent
  • DELETE /tasks/1 deletes and returns a confirmation message
  • pagination_params is used with Depends() (not inline)
  • task_filters is used with Depends() (not inline)
  • get_task_or_404 is used in GET, PATCH, and DELETE by ID
  • get_task_store is a yield dependency
  • get_task_or_404 uses Depends(get_task_store) (a sub-dependency)
  • The Paginator class-based dependency exists and works
  • dependency_overrides works in the test script

Connection with Module 2 (APIRouter and Middleware)

Your API now has reusable dependencies, but everything still lives in a single main.py file. In Module 2 (APIRouter and Middleware) you'll learn to:

Module 1 (now):                          Module 2 (next):
──────────────────                        ─────────────────────
Everything in main.py                 →   app/routers/tasks.py, app/routers/root.py
Inline dependencies                   →   app/dependencies/tasks.py, app/dependencies/pagination.py
No middleware                         →   Logging middleware, timing middleware
No lifecycle events                   →   Lifespan events (startup/shutdown)

The dependencies you created here will move into dedicated files. The endpoints will get organized into routers by domain. And middleware will intercept every request for logging and timing.


Additional resources

  1. FastAPI - Dependencies — The complete Dependency Injection tutorial
  2. FastAPI - Dependencies with yield — Yield dependencies for setup/cleanup
  3. FastAPI - Testing Dependencies — dependency_overrides for testing
  4. FastAPI - Sub-dependencies — Dependency chains
  5. FastAPI - Classes as Dependencies — Class-based dependencies
  6. Pydantic v2 - Models — Models for dependency results

What's next?

Module 1 complete. Your To-Do API now has DI applied: pagination encapsulated, lookup centralized, data access through a yield dependency, and testing with overrides. Each endpoint focuses on its business logic — the preconditions are handled by the dependencies.

In Module 2 (APIRouter and Middleware) you're going to modularize this app: split the endpoints into routers, organize the dependencies into dedicated files, add middleware for logging/timing, and configure lifecycle events. The transition: "Your code is DRY → now your app is modular."