FastAPI Crash Course: Modern, Fast Python APIs
Part 3 of the Python for Web & APIs track. Last updated: September 2026.
Flask taught you how APIs work. FastAPI is how most new Python APIs get built: you write plain Python with type hints, and FastAPI generates request validation, serialization, and interactive documentation automatically. It is fast (on par with Node.js in benchmarks), async-native, and the code reads like documentation. This post rebuilds the book catalog from Part 2 the FastAPI way.
Setup
python -m venv venv source venv/bin/activate # Windows: venv\Scripts\activate pip install fastapi "uvicorn[standard]"
Your first FastAPI app
Type hints are the whole game — they declare what each endpoint accepts:
from fastapi import FastAPI
app = FastAPI()
@app.get("/hello")
def hello():
return {"message": "Hello from FastAPI!"}
@app.get("/books/{book_id}")
def get_book(book_id: int): # FastAPI validates + converts book_id
return {"id": book_id, "title": f"Book #{book_id}"}
Run it with uvicorn main:app --reload (assuming the file is main.py). Request /books/abc and you get a 422 Unprocessable Entity with a precise JSON error — validation happened with zero code from you. Now visit http://127.0.0.1:8000/docs: a full interactive API console (Swagger UI) generated from your code. Try the endpoints right in the browser.
Query parameters and validation
Declare them as function arguments with defaults and constraints:
from fastapi import Query
@app.get("/search")
def search(
author: str = "",
limit: int = Query(default=10, ge=1, le=100), # 1 <= limit <= 100
):
return {"author": author, "limit": limit}
?limit=500 now returns a 422 explaining the constraint. That manual validate_book() function from Part 2? Gone.
Pydantic models: the heart of FastAPI
Request and response shapes are Pydantic models — classes with typed fields:
from pydantic import BaseModel, Field
class BookIn(BaseModel):
title: str = Field(min_length=1, max_length=200)
author: str = Field(min_length=1, max_length=100)
year: int | None = Field(default=None, ge=0, le=2100)
class Book(BookIn):
id: int
Send {"title": "", "year": "not-a-number"} and FastAPI rejects it with a 422 listing every problem. Invalid data can no longer reach your code — the framework stands guard at the door.
Project: the book catalog, FastAPI edition
The same API as Part 2 — notice how much validation code disappears:
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel, Field
app = FastAPI(title="Book Catalog API")
class BookIn(BaseModel):
title: str = Field(min_length=1, max_length=200)
author: str = Field(min_length=1, max_length=100)
year: int | None = Field(default=None, ge=0, le=2100)
class Book(BookIn):
id: int
books: dict[int, Book] = {
1: Book(id=1, title="Dune", author="Frank Herbert", year=1965),
2: Book(id=2, title="Neuromancer", author="William Gibson", year=1984),
}
next_id = 3
@app.get("/books", response_model=list[Book])
def list_books(author: str = ""):
result = list(books.values())
if author:
result = [b for b in result if author.lower() in b.author.lower()]
return result
@app.get("/books/{book_id}", response_model=Book)
def get_book(book_id: int):
if book_id not in books:
raise HTTPException(status_code=404, detail=f"No book with id {book_id}")
return books[book_id]
@app.post("/books", response_model=Book, status_code=201)
def create_book(payload: BookIn):
global next_id
book = Book(id=next_id, **payload.model_dump())
books[next_id] = book
next_id += 1
return book
@app.put("/books/{book_id}", response_model=Book)
def update_book(book_id: int, payload: BookIn):
if book_id not in books:
raise HTTPException(status_code=404, detail=f"No book with id {book_id}")
book = Book(id=book_id, **payload.model_dump())
books[book_id] = book
return book
@app.delete("/books/{book_id}", status_code=204)
def delete_book(book_id: int):
if book_id not in books:
raise HTTPException(status_code=404, detail=f"No book with id {book_id}")
del books[book_id]
return None
Three things to notice:
response_model— FastAPI filters every response through the model, so you never leak internal fields.HTTPException— the idiomatic way to return errors; FastAPI renders them as clean JSON.- Status codes as decorators —
status_code=201on the route, no tuple juggling.
Async endpoints (free performance)
Declare a route with async def and it runs on the event loop — while one request waits on a database or another API, others proceed:
import httpx
@app.get("/books/{book_id}/cover")
async def book_cover(book_id: int):
async with httpx.AsyncClient(timeout=10) as client:
# placeholder: pretend an external cover-art service exists
r = await client.get(f"https://api.example.com/covers/{book_id}")
r.raise_for_status()
return {"book_id": book_id, "cover": r.json()}
Rule of thumb: async def for I/O-bound work (network, DB calls); plain def is fine for CPU-bound code (FastAPI runs those in a threadpool).
Flask vs FastAPI: which to choose?
- FastAPI — new APIs, teams, anything where validation and docs matter. Automatic 422s, OpenAPI docs, async. The modern default.
- Flask — tiny services, maximum control, huge extension ecosystem, and learning how the web works (which is why this track starts there).
Both are production-grade. The skills transfer: routes, status codes, and JSON discipline are the same everywhere.
Key takeaways
- Type hints are executable: they give you validation, conversion, and docs for free.
- Pydantic models define your API contract — bad input gets a 422 before reaching your code.
response_modelguarantees your output shape;HTTPExceptionis how you raise clean JSON errors./docsgives you interactive documentation with zero extra work — use it, your API consumers will love you.async defroutes handle I/O-bound waiting efficiently.
Next in this series: Testing and Deploying Your Python API — prove it works with automated tests, then put it on the real internet.
Comments
Post a Comment