Document Error Responses
Add response models and error documentation to the get, delete, and update endpoints
Writing code and entering commands is only available on desktop. Open this page on a larger screen to complete this chapter.
The missing documentation
Open the /docs page and look at the GET /expenses/{expense_id} endpoint. The response section shows a generic "Successful Response" with no schema. A client developer cannot tell what fields to expect.
Worse, there is no mention of the 404 error. The client has no way to know that passing an invalid identifier returns {"detail": "Expense not found"} unless they test it manually.
Adding response_model and responses together
You can combine both parameters in a single decorator:
@app.get("/expenses/{expense_id}",
response_model=ExpenseOut,
responses={404: {"model": ErrorResponse}})- response_model documents the successful response shape
- responses documents additional status codes and their shapes
The /docs page then shows a complete contract: the 200 response returns an ExpenseOut object, and a 404 response returns an ErrorResponse object.
Three endpoints raise 404 errors: get, delete, and update. You will add both parameters to all three.
Instructions
Add response_model and responses to the three endpoints that raise 404 errors.
- Add
response_model=ExpenseOutandresponses={404: {"model": ErrorResponse}}to the@app.get("/expenses/{expense_id}")decorator. - Add
response_model=ExpenseOutandresponses={404: {"model": ErrorResponse}}to the@app.delete("/expenses/{expense_id}")decorator. - Add
response_model=ExpenseOutandresponses={404: {"model": ErrorResponse}}to the@app.patch("/expenses/{expense_id}")decorator.
import json
from datetime import datetime
from pathlib import Path
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel, Field, field_validator
from typing import Literal, Optional
app = FastAPI()
DATA_FILE = Path("expenses.json")
class Expense(BaseModel):
description: str = Field(min_length=1)
amount: float = Field(gt=0)
category: Literal["food", "transport", "entertainment", "utilities", "other"]
date: Optional[str] = None
@field_validator("date")
@classmethod
def validate_date_format(cls, v):
if v is not None:
try:
datetime.strptime(v, "%Y-%m-%d")
except ValueError:
raise ValueError("Date must be in YYYY-MM-DD format")
return v
class ExpenseUpdate(BaseModel):
description: Optional[str] = Field(default=None, min_length=1)
amount: Optional[float] = Field(default=None, gt=0)
category: Optional[Literal["food", "transport", "entertainment", "utilities", "other"]] = None
class ExpenseOut(BaseModel):
id: int
description: str
amount: float
category: str
date: Optional[str] = None
class ErrorResponse(BaseModel):
detail: str
def save_expenses():
data = {"counter": counter, "expenses": expenses}
DATA_FILE.write_text(json.dumps(data, indent=2))
def load_expenses():
global expenses, counter
if DATA_FILE.exists():
data = json.loads(DATA_FILE.read_text())
expenses = {int(k): v for k, v in data["expenses"].items()}
counter = data["counter"]
expenses = {}
counter = 0
load_expenses()
@app.post("/expenses", status_code=201, response_model=ExpenseOut)
def create_expense(expense: Expense):
global counter
counter += 1
expenses[counter] = {"id": counter, **expense.model_dump()}
save_expenses()
return expenses[counter]
@app.get("/expenses", response_model=list[ExpenseOut])
def list_expenses(category: str = None):
if category:
return [e for e in expenses.values() if e["category"] == category]
return list(expenses.values())
# Step 1: Add response_model=ExpenseOut and responses={404: {"model": ErrorResponse}} to this decorator
@app.get("/expenses/{expense_id}")
def get_expense(expense_id: int):
if expense_id not in expenses:
raise HTTPException(status_code=404, detail="Expense not found")
return expenses[expense_id]
# Step 2: Add response_model=ExpenseOut and responses={404: {"model": ErrorResponse}} to this decorator
@app.delete("/expenses/{expense_id}")
def delete_expense(expense_id: int):
if expense_id not in expenses:
raise HTTPException(status_code=404, detail="Expense not found")
save_expenses()
return expenses.pop(expense_id)
# Step 3: Add response_model=ExpenseOut and responses={404: {"model": ErrorResponse}} to this decorator
@app.patch("/expenses/{expense_id}")
def update_expense(expense_id: int, updates: ExpenseUpdate):
if expense_id not in expenses:
raise HTTPException(status_code=404, detail="Expense not found")
expenses[expense_id].update(updates.model_dump(exclude_unset=True))
save_expenses()
return expenses[expense_id]
Interactive Code Editor
Sign in to write and run code, track your progress, and unlock all chapters.
Sign In to Start Coding