Let’s put everything together. This is a complete user management app — the same patterns you’ve learned across the last several lessons, wired together into one working application.
This walkthrough is a JS-first teaching version of the patterns. The companion repo is more production-structured: backend routes live in backend/routers/users.py, models in backend/models.py, the web client is TypeScript (.ts/.tsx with frontend/src/types.ts and frontend/src/api/users.ts), and there’s also a mobile/ Expo app using the same FastAPI API.
The companion repo uses UserCreate for both create and update. This walkthrough shows a separate UserUpdate model to demonstrate a common partial-update pattern you’ll likely adopt later.
Copy
# backend/main.pyfrom fastapi import FastAPI, HTTPExceptionfrom fastapi.middleware.cors import CORSMiddlewarefrom pydantic import BaseModelimport osapp = FastAPI()ALLOWED_ORIGINS = os.getenv("ALLOWED_ORIGINS", "http://localhost:5173").split(",")app.add_middleware( CORSMiddleware, allow_origins=ALLOWED_ORIGINS, allow_credentials=True, allow_methods=["*"], allow_headers=["*"],)# ── Models ──────────────────────────────────────class UserCreate(BaseModel): name: str email: strclass UserUpdate(BaseModel): name: str | None = None email: str | None = None# ── In-memory storage (use a real DB in production) ──users = [ {"id": 1, "name": "Sarah Chen", "email": "sarah@example.com"}, {"id": 2, "name": "John Park", "email": "john@example.com"},]next_id = 3def find_user(user_id: int): return next((u for u in users if u["id"] == user_id), None)# ── Endpoints ───────────────────────────────────@app.get("/api/users")def get_users(): return users@app.get("/api/users/{user_id}")def get_user(user_id: int): user = find_user(user_id) if not user: raise HTTPException(status_code=404, detail="User not found") return user@app.post("/api/users", status_code=201)def create_user(data: UserCreate): global next_id new_user = {"id": next_id, "name": data.name, "email": data.email} users.append(new_user) next_id += 1 return new_user@app.put("/api/users/{user_id}")def update_user(user_id: int, data: UserUpdate): user = find_user(user_id) if not user: raise HTTPException(status_code=404, detail="User not found") if data.name is not None: user["name"] = data.name if data.email is not None: user["email"] = data.email return user@app.delete("/api/users/{user_id}", status_code=204)def delete_user(user_id: int): user = find_user(user_id) if not user: raise HTTPException(status_code=404, detail="User not found") users.remove(user)
Five endpoints. Five HTTP methods. That’s the entire backend for a CRUD app.
Pydantic validates the data. If valid, creates the user and returns { id: 3, name: "Maria Lopez", email: "maria@example.com" } with status 201
5
API client parses the response
response.json() returns the new user object back to handleCreate
6
State updates, React re-renders
setUsers(prev => [...prev, newUser]) adds the user. React re-renders the list. Maria appears on screen.
Every CRUD operation follows this same flow: Component → API client → fetch() → FastAPI → response → state update → re-render.
When debugging, trace the flow step by step. Add console.log at each stage to see where data gets lost or transformed incorrectly. The Network tab in DevTools shows the actual HTTP requests and responses.