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:
- A
Taskmodel withtitle(required),done(bool, defaultFalse), andpriority(int, 1–5, default 3), validated with Pydantic. GET /tasks— list all tasks, with an optionaldonequery parameter to filter.GET /tasks/{task_id}— return one task, or a404if it doesn't exist.POST /tasks— create a task and return it with status code201.PUT /tasks/{task_id}— update a task, or a404if it doesn't exist.DELETE /tasks/{task_id}— delete a task and return status code204, or a404if it doesn't exist.- A
GET /tasks/statsroute 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:
- Create at least 3 tasks with different priorities using POST.
- List all tasks and print them.
- Mark one task as done using PUT.
- Fetch
/tasks/statsand print the summary. - 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
tasksdictionary?
Grading Rubric
| Criterion | Weight | Description |
|---|---|---|
| Server implementation | 40% | All 7 required routes implemented, validated, and returning correct status codes. |
| Client implementation | 30% | Client script exercises every route and handles at least one error case. |
| Reflection & code quality | 30% | Thoughtful answers; readable code; runs top-to-bottom without manual edits. |