Once you know how to define Pydantic models, you can practice CRUD with a tiny in-memory store. CRUD stands for Create, Read, Update, Delete—the four operations every data app must handle reliably. Instead of introducing a database right away, this post repeats the API loop with a Python list to cement the flow.
Key terms
- CRUD – the core create/read/update/delete operations you implement throughout this article.
- HTTP status 201/204 – "Created" and "Success with no content," respectively; they clarify what happened in each endpoint.
- HTTPException – FastAPI's helper for returning a specific status code and message immediately, such as 404.
- PATCH – an HTTP method used here to update only part of a record, like moving a task across statuses.
Practice card
- Estimated time: 45 minutes
- Prereqs: Part 4 Pydantic example, Python 3.12, Swagger UI or
curl- Goal: implement list-backed CRUD endpoints and return distinct status codes
Save the code snippets into main.py, run uv run fastapi dev main.py (or uv run uvicorn main:app --reload), and open http://127.0.0.1:8000/docs to exercise every route.
What this post covers
- Use a global list as a simple store
- Write POST/GET/PUT/DELETE endpoints
- Increment IDs and update tasks reliably
Starter code
from enum import Enum
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
class Status(str, Enum):
todo = "todo"
doing = "doing"
done = "done"
app = FastAPI()
class Task(BaseModel):
title: str
done: bool = False
status: Status = Status.todo
tasks: list[dict] = []
current_id = 0
tasks holds the data as dictionaries, and current_id increments to assign identifiers. This store lives only in memory: it resets on every restart and is not thread-safe, so treat it purely as a practice scaffold.
Create
@app.post("/tasks", status_code=201)
def create_task(task: Task):
global current_id
current_id += 1
record = {"id": current_id, **task.model_dump()}
tasks.append(record)
return record
status_code=201signals a new resource.model_dump()turns the Pydantic model into a dictionary (use.dict()if you are still on Pydantic 1.x).- The
global current_idpattern works for one-process experiments only; later you will rely on database-generated IDs instead.
Read
@app.get("/tasks")
def list_tasks():
return tasks
@app.get("/tasks/{task_id}")
def get_task(task_id: int):
for t in tasks:
if t["id"] == task_id:
return t
raise HTTPException(status_code=404, detail="Task not found")
Return the full list or a single task; missing IDs become 404s via HTTPException.
Update
@app.put("/tasks/{task_id}")
def update_task(task_id: int, task: Task):
for idx, t in enumerate(tasks):
if t["id"] == task_id:
updated = {"id": task_id, **task.model_dump()}
tasks[idx] = updated
return updated
raise HTTPException(status_code=404, detail="Task not found")
Return the updated record so the client sees the new state immediately.
PUT replaces the entire resource—clients must send every field. For partial tweaks, we will add a PATCH route shortly.
Delete
@app.delete("/tasks/{task_id}", status_code=204)
def delete_task(task_id: int):
for idx, t in enumerate(tasks):
if t["id"] == task_id:
tasks.pop(idx)
return
raise HTTPException(status_code=404, detail="Task not found")
Use 204 when no response body is needed.
CRUD flow overview
Regardless of whether you drive the flow from Swagger UI or curl, this loop repeats: create, inspect, update, delete, repeat.
If your editor does not render the D2 diagram, remember the same order: client sends JSON to create/update, FastAPI mutates the in-memory list, and every response echoes the current list or single record.
Example: project schedule management
Extend the model with a status field (already present in Task) and use PATCH to change only that field.
class StatusUpdate(BaseModel):
status: Status
@app.patch("/tasks/{task_id}")
def move_task(task_id: int, update: StatusUpdate):
for task in tasks:
if task["id"] == task_id:
task["status"] = update.status
return task
raise HTTPException(status_code=404, detail="Task not found")
PATCH now accepts a minimal JSON body such as { "status": "doing" }, validates it through StatusUpdate, and leaves the rest of the record untouched.
Why list-based practice matters
- Skip database setup and focus on API behavior.
- Rehearse the full CRUD sequence repeatedly in Swagger UI.
- Reuse the same signatures and validation rules when you later plug in persistence.
- Accept that data disappears whenever the process restarts and that concurrent requests may race—perfect reminders that you need a real database for production.
Practice
- Follow along: implement all
/tasksCRUD routes and run POST → GET → PUT → DELETE in Swagger UI. - Follow along: implement all
/tasksCRUD routes, run the server, open/docs, and execute POST → GET → PUT → DELETE in order so you can watch IDs increment. - Extend: keep 201 and 204 responses correct and add the PATCH route; send
{ "status": "doing" }to confirm partial updates. - Debug: call a missing ID and confirm you get a 404 with the expected message.
- Done when: success and error paths both return the right status codes and you have a recorded test order.
Wrap-up
An in-memory list is enough to internalize CRUD mechanics. Next we will tighten response models and documentation so clients always receive consistent structures.
When you eventually swap in a database, keep the same Task schema, add response_model=Task (or list variants) to each route, and let FastAPI keep enforcing those contracts for you.
💬 댓글
이 글에 대한 의견을 남겨주세요