Write the POST Endpoint
Define the POST route and function that receives an Expense
Writing code and entering commands is only available on desktop. Open this page on a larger screen to complete this chapter.
Where do expenses go?
Your API can validate expenses, but it has nowhere to put them. You need two things:
- A storage dictionary (
expenses = {}): holds every expense, keyed by its unique identifier. - An identifier counter (
counter = 0): tracks the next identifier to assign. Each new expense getscounter + 1.
A dictionary works well here because you can look up any expense by its identifier in one step: expenses[1] returns expense number 1 instantly.
Decorators and status codes
The @app.post decorator registers a function to handle POST requests at a given URL path:
@app.post("/expenses", status_code=201)
def create_expense(expense: Expense):
...The status_code=201 parameter tells FastAPI to return HTTP status 201 Created instead of the default 200. Status 201 signals to the client that a new resource was created, not just retrieved.
Type hint triggers validation
The expense: Expense parameter does all the work. FastAPI sees the Pydantic type and automatically:
- Reads JSON from the request body.
- Validates it against the
Expensemodel. - Returns a
422error if the data is invalid.
Your function only runs if the data passes every validation rule you defined in your Expense model.
Instructions
Set up the storage and define the POST endpoint signature.
- Below the
Expenseclass, add the storage dictionaryexpenses = {}. - Below
expenses, add the identifier countercounter = 0. - Add the decorator
@app.post("/expenses", status_code=201). - Define a function named
create_expensewith anexpenseparameter of typeExpense. Usepassas the function body.
from datetime import datetime
from fastapi import FastAPI
from pydantic import BaseModel, Field, field_validator
from typing import Literal, Optional
app = FastAPI()
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
# Step 1: Add the storage dictionary expenses = {}
# Step 2: Add the identifier counter counter = 0
# Step 3: Add @app.post("/expenses", status_code=201)
# Step 4: Define create_expense with expense: Expense parameter
Interactive Code Editor
Sign in to write and run code, track your progress, and unlock all chapters.
Sign In to Start Coding