Blog · Jul 10, 2026
Getting Started with FastAPI
Build a real in-memory notes API — GET, POST, DELETE, validation errors, 404s — and check every response code with curl, not vibes.
FastAPI is a pleasant way to wrap models and scripts behind HTTP. Auto docs, type hints, and Pydantic validation make it a strong default for LLM demos and internal tools. This post builds a small but real API with four routes and deliberately exercises its error paths — a 404 and a 422 you trigger yourself, not just the happy path.
Install
pip install fastapi uvicorn
Mini project: a notes API with real validation and error handling
# main.py
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel, Field
app = FastAPI(title="Notes API")
class Note(BaseModel):
id: int
text: str
class NoteIn(BaseModel):
text: str = Field(min_length=1, max_length=500)
notes: dict[int, Note] = {}
next_id = 1
@app.get("/health")
def health():
return {"ok": True}
@app.get("/notes")
def list_notes():
return list(notes.values())
@app.post("/notes", status_code=201)
def create_note(body: NoteIn):
global next_id
note = Note(id=next_id, text=body.text)
notes[next_id] = note
next_id += 1
return note
@app.get("/notes/{note_id}")
def get_note(note_id: int):
if note_id not in notes:
raise HTTPException(status_code=404, detail="note not found")
return notes[note_id]
@app.delete("/notes/{note_id}", status_code=204)
def delete_note(note_id: int):
if note_id not in notes:
raise HTTPException(status_code=404, detail="note not found")
del notes[note_id]
Run it and open the auto-generated docs:
uvicorn main:app --reload --port 8000
# open http://127.0.0.1:8000/docs
Now exercise every path with curl, including the two that should fail on purpose:
curl -s http://127.0.0.1:8000/health
# {"ok":true}
curl -s -X POST http://127.0.0.1:8000/notes \
-H 'content-type: application/json' -d '{"text":"buy milk"}'
# {"id":1,"text":"buy milk"} — check the HTTP status too:
curl -s -o /dev/null -w "%{http_code}\n" -X POST http://127.0.0.1:8000/notes \
-H 'content-type: application/json' -d '{"text":"buy eggs"}'
# 201, not the default 200 — because of status_code=201 on the route
curl -s http://127.0.0.1:8000/notes
# [{"id":1,...},{"id":2,...}]
curl -s -o /dev/null -w "%{http_code}\n" http://127.0.0.1:8000/notes/999
# 404 — an id that was never created
curl -s -X POST http://127.0.0.1:8000/notes \
-H 'content-type: application/json' -d '{"text":""}' \
-o /dev/null -w "%{http_code}\n"
# 422 — Pydantic's min_length=1 rejected it before your code ever ran
curl -s -X DELETE http://127.0.0.1:8000/notes/1 -o /dev/null -w "%{http_code}\n"
# 204
If any of those status codes don’t match, that’s the bug to chase — the point of writing them down is having something concrete to check against, not “it looked like it worked in the docs UI.”
Why it fits AI work
| Need | FastAPI habit |
|---|---|
| Validate inputs before they hit your model | Pydantic models (the NoteIn pattern above) |
| Long model calls | background tasks or a job queue, not a blocking route |
| Ship consistently | Docker + uvicorn |
| Explore the API while building | /docs (Swagger UI) — try requests directly from the browser |
Keep inference or business logic in a plain Python module; the route itself should stay thin: validate → call → return.
Common footguns
- In-memory state isn’t persistence — the
notesdict resets to empty every timeuvicorn --reloadrestarts (which it does automatically on file changes). This is fine for learning; for anything real, back it with the SQLite schema from Getting Started with SQL. - Blocking code inside
async def— a synchronous call liketime.sleep()or heavy file I/O inside anasync defroute blocks the entire event loop, stalling every other request. Use a plaindefroute for sync work (FastAPI runs it in a thread pool automatically) orawaita genuinely async call. - Forgetting the default status code — routes return
200unless you setstatus_code=explicitly; aPOSTthat creates a resource should return201, aDELETEtypically204. - Trusting raw
dict/request.json()instead of Pydantic — skips validation entirely, so malformed input reaches your business logic instead of failing fast with a clear422. - CORS errors when calling from a browser on a different origin — add
CORSMiddlewareexplicitly; FastAPI doesn’t allow cross-origin requests by default, on purpose. - Leaving
--reloadon outside development — it watches the filesystem and restarts the process, which is convenient locally and inappropriate (and slower) in production.
You know you’re done when…
GET /healthreturns{"ok": true}with status200POST /noteswith a valid body returns status201and an incrementingidGET /notes/999(an id that doesn’t exist) returns404, not a500or an empty200POST /noteswith{"text": ""}returns422because of Pydantic’smin_length- You can explain why restarting
uvicorn --reloadwipes your notes, and what you’d change to make them survive a restart
Next
Serve a tiny model after LLM Mastery (quantization & serving in article 44). For product UIs, pair with Getting Started with JavaScript.