Skip to main content

Capstone: Task Manager API

Introduction​

This capstone connects both halves of the APIs section into one working system: a FastAPI server (main.py) and a requests-based client (client.py) that together demonstrate every concept from the previous two chapters — path/query parameters, Pydantic validation, full CRUD, status codes, and defensive client-side error handling.

Build the server first, confirm each route works via /docs, then write the client against it.


Concepts​

Nothing new here — this chapter is deliberately concept-free. Everything you need was covered in Consuming APIs with requests and Building APIs with FastAPI; the goal now is combining it into one coherent, tested system rather than isolated snippets.


Challenges​

1. Server: main.py​

Build a complete Task Manager service satisfying all of the following:

  1. A Task model with title (required), done (bool, default False), and priority (int, 1–5, default 3), validated with Pydantic.
  2. GET /tasks — list all tasks, with an optional done query parameter to filter.
  3. GET /tasks/{task_id} — return one task, or a 404 if it doesn't exist.
  4. POST /tasks — create a task and return it with status code 201.
  5. PUT /tasks/{task_id} — update a task, or a 404 if it doesn't exist.
  6. DELETE /tasks/{task_id} — delete a task and return status code 204, or a 404 if it doesn't exist.
  7. A GET /tasks/stats route returning the count of total, done, and pending tasks.
💡 Show Answer
# main.py
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel, Field
from typing import Optional

app = FastAPI(title='Task Manager API')
tasks: dict[int, dict] = {}
next_id = 1

class Task(BaseModel):
title: str = Field(min_length=1)
done: bool = False
priority: int = Field(default=3, ge=1, le=5)

# Note: this route must be declared before /tasks/{task_id}, or FastAPI will
# try to parse "stats" as an int task_id and return a 422 instead.
@app.get('/tasks/stats')
def task_stats():
done_count = sum(1 for t in tasks.values() if t['done'])
return {
'total': len(tasks),
'done': done_count,
'pending': len(tasks) - done_count,
}

@app.get('/tasks')
def list_tasks(done: Optional[bool] = None):
if done is None:
return tasks
return {tid: t for tid, t in tasks.items() if t['done'] == done}

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

@app.post('/tasks', status_code=201)
def create_task(task: Task):
global next_id
tasks[next_id] = {**task.model_dump(), 'id': next_id}
next_id += 1
return tasks[next_id - 1]

@app.put('/tasks/{task_id}')
def update_task(task_id: int, task: Task):
if task_id not in tasks:
raise HTTPException(status_code=404, detail='Task not found')
tasks[task_id] = {**task.model_dump(), 'id': task_id}
return tasks[task_id]

@app.delete('/tasks/{task_id}', status_code=204)
def delete_task(task_id: int):
if task_id not in tasks:
raise HTTPException(status_code=404, detail='Task not found')
del tasks[task_id]

2. Client: client.py​

Write a requests-based client satisfying all of the following:

  1. Create at least 3 tasks with different priorities using POST.
  2. List all tasks and print them.
  3. Mark one task as done using PUT.
  4. Fetch /tasks/stats and print the summary.
  5. Delete one task and confirm it is gone by requesting it again and handling the 404.
💡 Show Answer
# client.py
import requests

BASE_URL = 'http://127.0.0.1:8000'

created_ids = []
for title, priority in [('Write report', 1), ('Review PR', 3), ('Plan sprint', 5)]:
r = requests.post(f'{BASE_URL}/tasks', json={'title': title, 'priority': priority})
created_ids.append(r.json()['id'])
print('Created:', r.status_code, r.json())

print('All tasks:', requests.get(f'{BASE_URL}/tasks').json())

done_id = created_ids[0]
updated = requests.put(f'{BASE_URL}/tasks/{done_id}', json={'title': 'Write report', 'done': True, 'priority': 1})
print('Marked done:', updated.json())

print('Stats:', requests.get(f'{BASE_URL}/tasks/stats').json())

deleted_id = created_ids[-1]
requests.delete(f'{BASE_URL}/tasks/{deleted_id}')
check = requests.get(f'{BASE_URL}/tasks/{deleted_id}')
if check.status_code == 404:
print('Confirmed deleted:', check.json()['detail'])

Reflection Questions​

Answer in 2–3 sentences each — there's no hidden answer for these, they're meant to be discussed with an instructor or peer:

  • What did FastAPI validate automatically that you would have had to check by hand in plain Python?
  • Where in your client did you need explicit error handling, and why didn't the server just handle it for you?
  • If this API needed to support thousands of concurrent users, what would you change about the in-memory tasks dictionary?

Grading Rubric​

CriterionWeightDescription
Server implementation40%All 7 required routes implemented, validated, and returning correct status codes.
Client implementation30%Client script exercises every route and handles at least one error case.
Reflection & code quality30%Thoughtful answers; readable code; runs top-to-bottom without manual edits.