Contents
22 Chapter

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:

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.

DjangoFlaskFastAPI
StyleFull-stack (batteries included)MicroMicro + types
AsyncPartial (4.0+)External libraryNative
Type-hint usageAuxiliaryAuxiliaryCore behavior
Auto docs (OpenAPI)Separate librarySeparateBuilt-in
Learning curveSteepVery flatFlat
Data validationDRF + manualManualPydantic, 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.

Create the project
uv init todo-api --python 3.14
cd todo-api

Install dependencies #

Runtime 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-validatorEmailStr validation
  • python-multipart — form / file uploads
  • fastapi-cli — the fastapi CLI command

Next, dev tools.

Dev tools
uv add --dev pytest pytest-asyncio ruff pyright

Chapter 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 #

app/main.py
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 #

Dev mode
uv run fastapi dev app/main.py

What fastapi dev does:

  • Spins up uvicorn
  • Auto-reloads on code changes
  • Debug logs
  • Doesn’t expose to the LAN (127.0.0.1 only)

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 #

Multiple methods / paths
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 argument item_id: int
    • If it isn’t an int, 422 Unprocessable Entity is returned automatically
  • Query parameters skip, limit → function arguments with defaults
    • Auto-parsed in the form ?skip=10&limit=20
  • 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:

🚫 Compared to 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 #

Advanced 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>= 1
  • le=10000<= 10000
  • min_length=3 — at least 3 characters
  • description=... — 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 #

Returning a dataclass / Pydantic model
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 #

Error responses
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.

Base structure (the shape Part 4 fills in)
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.py

In 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.

Install
uv add pydantic-settings
app/core/config.py
from 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.

.env
JWT_SECRET=production-secret-here
DATABASE_URL=postgresql+asyncpg://user:pw@localhost/todo

Exclude .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 #

app/main.py — wrap-up 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"}
Run
uv run fastapi dev app/main.py
# → http://127.0.0.1:8000
# → http://127.0.0.1:8000/docs

Two endpoints + auto-generated docs + environment variables separated. The starting point for the next chapter.

Exercises #

  1. Create the project with uv init todo-api --python 3.14 and add fastapi[standard]. In app/main.py write three endpoints — GET /, GET /items/{item_id}, GET /items?skip&limit — then run it with fastapi dev and confirm Swagger UI is auto-generated at /docs.
  2. Add range validation to item_id with Annotated[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.
  3. Write app/core/config.py with pydantic-settings and have it read app_name / database_url from .env. Confirm that a bad-type environment variable (e.g. an empty string for database_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 dev for auto-reload. The function signature is the route spec, /docs for Swagger automatically. Annotated[T, Path(...)] / Annotated[T, Query(...)] for stricter validation, datetime and friends auto-serialized when returning Pydantic models, HTTPException for errors, pydantic-settings to 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.

X