FastAPI setup and getting started
Why FastAPI, first project setup with uv, Hello FastAPI, automatic OpenAPI/Swagger UI generation — all in one place.
This is the start of Part 4, FastAPI in Production. It’s the stage where all 21 chapters from Part 1 (Getting Started) → Part 2 (Structuring) → Part 3 (Depth and Concurrency) come together. As you build one project with FastAPI, you bring everything you’ve sharpened so far into one place — type hints, dataclass / Pydantic, async / await, dependency injection, and testing.
The shape of Part 4:
- Chapter 22 Setup and getting started, OpenAPI ← this chapter
- Chapter 23 Routing, Pydantic models, dependency injection
- Chapter 24 Pydantic v2 in depth ★new
- Chapter 25 Connecting a DB — SQLAlchemy 2.x + Alembic
- Chapter 26 Authentication — OAuth2 password flow + JWT
- Chapter 27 Async and background jobs
- Chapter 28 Testing and deploy — pytest, Docker, Railway/Fly
- Chapter 29 Capstone — finishing the TODO API ★new
What you’ll build is a small Todo API — user sign-up / login, todo CRUD, async background work, testing, and deployment. Every chapter adds one more layer on top.
Why FastAPI #
There are several Python web frameworks. Django, Flask, FastAPI, Litestar, Starlette, etc. Each has its strengths.
| Django | Flask | FastAPI | |
|---|---|---|---|
| Style | Full-stack (batteries included) | Micro | Micro + types |
| Async | Partial (4.0+) | External library | Native |
| Type-hint usage | Auxiliary | Auxiliary | Core behavior |
| Auto docs (OpenAPI) | Separate library | Separate | Built-in |
| Learning curve | Steep | Very flat | Flat |
| Data validation | DRF + manual | Manual | Pydantic, automatic |
FastAPI’s strength is that type hints just work. Write a function signature and that becomes:
- Input validation
- Serialization / deserialization
- An OpenAPI schema (Swagger UI auto-generated)
- Editor autocomplete
- Static type checking
All five at once. Other frameworks require separate configuration for each. FastAPI is where the type hints we sharpened in this book hit hardest, which is why we set it as the convergence point of the book.
Project setup #
Start with the uv flow from Chapter 1.
uv init todo-api --python 3.14
cd todo-apiInstall dependencies #
uv add "fastapi[standard]"What that single fastapi[standard] line brings in:
- FastAPI — the framework itself
- uvicorn — the ASGI server (dev / prod)
- pydantic — data validation
- httpx — test client (used in Chapter 28)
- email-validator —
EmailStrvalidation - python-multipart — form / file uploads
- fastapi-cli — the
fastapiCLI command
Next, dev tools.
uv add --dev pytest pytest-asyncio ruff pyrightChapter 30 Type checker setup and CI integration covers the formal setup for these tools. The standard for FastAPI testing is pytest + httpx (covered in detail in Chapter 28 Testing and deploy).
First endpoint #
from fastapi import FastAPI
app = FastAPI(
title="Todo API",
version="0.1.0",
description="Example API for the Modern Python book",
)
@app.get("/")
def read_root() -> dict[str, str]:
return {"hello": "fastapi"}That’s it. Seven lines of Python and you have an HTTP server.
Run it #
uv run fastapi dev app/main.pyWhat fastapi dev does:
- Spins up uvicorn
- Auto-reloads on code changes
- Debug logs
- Doesn’t expose to the LAN (
127.0.0.1only)
Open http://127.0.0.1:8000 in a browser:
{"hello": "fastapi"}More importantly — automatic docs #
Go to /docs on the same server. Swagger UI is sitting there automatically. The endpoint list, input schemas, response models, and even a “Try it out” button that calls them right there. Without writing a single extra line.
Visit /redoc and you get a ReDoc-style document alongside it.
This is what FastAPI’s auto docs really mean. Function signatures + type hints + docstrings are converted to an OpenAPI spec as-is.
Richer endpoints #
from fastapi import FastAPI
app = FastAPI(title="Todo API", version="0.1.0")
@app.get("/")
def read_root() -> dict[str, str]:
"""Root — for health check."""
return {"hello": "fastapi"}
@app.get("/items/{item_id}")
def read_item(item_id: int) -> dict[str, int]:
"""Look up a single item by ID."""
return {"item_id": item_id}
@app.get("/items")
def list_items(skip: int = 0, limit: int = 10) -> dict[str, int]:
"""Pagination query parameters."""
return {"skip": skip, "limit": limit}The automatic conversion happening here:
- Path parameter
{item_id}→ function argumentitem_id: int- If it isn’t an
int, 422 Unprocessable Entity is returned automatically
- If it isn’t an
- Query parameters
skip,limit→ function arguments with defaults- Auto-parsed in the form
?skip=10&limit=20
- Auto-parsed in the form
- Return type
-> dict[str, int]→ JSON response - docstring → description in Swagger UI
The key is that you wrote int and the conversion / validation happens automatically. In Flask:
@app.route("/items/<int:item_id>")
def read_item(item_id):
return jsonify({"item_id": item_id})You have to write the type in the route like <int:item_id>, and handle validation-failure responses yourself. In FastAPI, the function signature is the route specification.
Path, Query — finer-grained validation
#
from fastapi import FastAPI, Path, Query
from typing import Annotated
app = FastAPI()
@app.get("/items/{item_id}")
def read_item(
item_id: Annotated[int, Path(ge=1, le=10000, description="Item ID")],
q: Annotated[str | None, Query(min_length=3, max_length=50)] = None,
):
return {"item_id": item_id, "q": q}Path() and Query() add validation rules and metadata.
ge=1—>= 1le=10000—<= 10000min_length=3— at least 3 charactersdescription=...— shown in Swagger docs
The Annotated[T, ...] pattern is exactly the one from Chapter 20 Advanced typing — the standard for binding metadata to a type. Almost all of FastAPI’s dependency injection is built on this pattern.
Responses — automatic serialization #
from datetime import datetime, timezone
from pydantic import BaseModel
class Todo(BaseModel):
id: int
title: str
done: bool
created_at: datetime
@app.get("/todos/1")
def get_todo() -> Todo:
return Todo(
id=1,
title="Learn FastAPI",
done=False,
created_at=datetime.now(timezone.utc),
)Response:
{
"id": 1,
"title": "Learn FastAPI",
"done": false,
"created_at": "2026-05-01T12:00:00+00:00"
}Non-JSON types like datetime are auto-converted. Pydantic handles serialization. Writing this by hand means conversion code per route, but with FastAPI you only write the return type.
For details on the Todo model, see Chapter 23 Routing, Pydantic models, dependency injection and Chapter 24 Pydantic v2 in depth.
Errors — HTTPException
#
from fastapi import HTTPException
todos: dict[int, Todo] = {1: Todo(...)}
@app.get("/todos/{todo_id}")
def get_todo(todo_id: int) -> Todo:
if todo_id not in todos:
raise HTTPException(status_code=404, detail="Todo not found")
return todos[todo_id]A single line of raise HTTPException(...) produces a JSON error response.
{"detail": "Todo not found"}With the appropriate HTTP status code (404, 400, 500, etc.). Custom exception handlers come up later in Part 4.
Project structure — tidy from the start #
Even for a small API, splitting things into modules / packages from the start carries forward the flow from Chapter 7 Modules, packages, and pyproject.toml.
todo-api/
├── pyproject.toml
├── app/
│ ├── __init__.py
│ ├── main.py # FastAPI instance, register routers
│ ├── core/
│ │ ├── config.py # environment variables
│ │ └── security.py # JWT, passwords (Chapter 26)
│ ├── api/
│ │ ├── __init__.py
│ │ ├── deps.py # dependency injection (Chapters 23, 26)
│ │ ├── todos.py # todo router
│ │ └── auth.py # auth router (Chapter 26)
│ ├── models/ # SQLAlchemy models (Chapter 25)
│ │ └── todo.py
│ ├── schemas/ # Pydantic schemas (Chapters 23, 24)
│ │ └── todo.py
│ └── db/ # DB connection (Chapter 25)
│ └── session.py
├── alembic/ # migrations (Chapter 25)
└── tests/ # pytest (Chapter 28)
└── test_todos.pyIn this chapter it’s a single app/main.py, but from the next chapter on we’ll fill in one directory at a time. By Chapter 29 Capstone — finishing the TODO API the full shape is in place.
Environment variables — pydantic-settings
#
The standard is not to bake settings (DB URL, JWT secret, etc.) into code, but to pull them out as environment variables. The standard tool in the FastAPI ecosystem is pydantic-settings.
uv add pydantic-settingsfrom pydantic_settings import BaseSettings, SettingsConfigDict
class Settings(BaseSettings):
app_name: str = "Todo API"
database_url: str = "sqlite+aiosqlite:///./todo.db"
jwt_secret: str = "dev-only-change-me"
jwt_expire_minutes: int = 60
model_config = SettingsConfigDict(env_file=".env", env_file_encoding="utf-8")
settings = Settings()It auto-reads from a .env file, and environment variables override what’s there. Type validation is included, so a bad environment variable is blocked at startup.
JWT_SECRET=production-secret-here
DATABASE_URL=postgresql+asyncpg://user:pw@localhost/todoExclude .env from git (add it to .gitignore). Be careful with security — Chapter 31 Logging and observability covers how to keep secrets out of logs.
A quick recap — the result of this chapter #
from fastapi import FastAPI, HTTPException
from app.core.config import settings
app = FastAPI(
title=settings.app_name,
version="0.1.0",
)
@app.get("/")
def read_root() -> dict[str, str]:
return {"app": settings.app_name, "status": "ok"}
@app.get("/health")
def health() -> dict[str, str]:
return {"status": "healthy"}uv run fastapi dev app/main.py
# → http://127.0.0.1:8000
# → http://127.0.0.1:8000/docsTwo endpoints + auto-generated docs + environment variables separated. The starting point for the next chapter.
Exercises #
- Create the project with
uv init todo-api --python 3.14and addfastapi[standard]. Inapp/main.pywrite three endpoints —GET /,GET /items/{item_id},GET /items?skip&limit— then run it withfastapi devand confirm Swagger UI is auto-generated at/docs. - Add range validation to
item_idwithAnnotated[int, Path(ge=1, le=100)]. Confirm that calling with an out-of-range ID returns 422 automatically. Also confirm the validation rules show up in Swagger UI’s input form. - Write
app/core/config.pywithpydantic-settingsand have it readapp_name/database_urlfrom.env. Confirm that a bad-type environment variable (e.g. an empty string fordatabase_url) fails at startup.
In one line: FastAPI’s strength is that type hints work as-is for validation / serialization / docs / autocomplete. A single
uv add "fastapi[standard]"brings in uvicorn / pydantic / httpx.fastapi devfor auto-reload. The function signature is the route spec,/docsfor Swagger automatically.Annotated[T, Path(...)]/Annotated[T, Query(...)]for stricter validation, datetime and friends auto-serialized when returning Pydantic models,HTTPExceptionfor errors,pydantic-settingsto separate env vars, module / package structure from the start.
Next chapter #
Next, Chapter 23 Routing, Pydantic models, dependency injection covers splitting routes with APIRouter, using Pydantic schemas in earnest, and factoring shared logic across routes (authenticated user, DB session, etc.) out through dependency injection.