Spending Summary
Build a capstone endpoint that calculates total spending per category
Writing code and entering commands is only available on desktop. Open this page on a larger screen to complete this chapter.
The need for server-side aggregation
A client that wants to display total spending per category has two options: download every expense and sum the amounts locally, or ask the server to do the math. The first option wastes bandwidth and pushes work onto the client. The second option is a single request that returns a small dictionary.
Server-side aggregation is a common pattern in APIs. The server already has all the data in memory, so the calculation is fast. The client gets a compact result without transferring records it does not need.
How the summary works
The endpoint loops through every expense and groups the amounts by category. The result is a dictionary where each key is a category name and each value is the total amount:
{"food": 42.50, "transport": 15.00, "entertainment": 8.99}Python's dict.get() method handles categories that appear for the first time. summary.get("food", 0) returns the current total for food, or 0 if food has not appeared yet.
Choosing the route path
The endpoint uses /summary rather than /expenses/summary. Placing it under /expenses would conflict with the {expense_id} path parameter. FastAPI would try to parse "summary" as an integer and return a validation error.
Instructions
Add a spending summary endpoint at the bottom of the file.
- Add the
@app.get("/summary")decorator. - Define a function named
spending_summarythat takes no arguments. - Build the summary: create an empty dictionary named
summary, then loop throughexpenses.values(). For each expense, readexpense["category"]into a variable namedcat. Setsummary[cat]tosummary.get(cat, 0) + expense["amount"]. - Return
summary.
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())
@app.get("/expenses/{expense_id}", response_model=ExpenseOut, responses={404: {"model": ErrorResponse}})
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]
@app.delete("/expenses/{expense_id}", response_model=ExpenseOut, responses={404: {"model": ErrorResponse}})
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)
@app.patch("/expenses/{expense_id}", response_model=ExpenseOut, responses={404: {"model": ErrorResponse}})
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]
# Step 1: Add @app.get("/summary")
# Step 2: Define spending_summary
# Step 3: Build summary dict by looping expenses and summing amounts per category
# Step 4: Return summary
Interactive Code Editor
Sign in to write and run code, track your progress, and unlock all chapters.
Sign In to Start Coding