# FastAPI From Zero

> Learn the modern Python API framework the way it's actually used: type-hint-driven routing, Pydantic models and automatic validation, response models, the Depends() dependency system, async and concurrency done right, databases, authentication with OAuth2/JWT, testing, and deployment. Plus the killer feature - automatic interactive docs - explained, not just shown.


---

# FastAPI From Zero

FastAPI is the framework that made building Python APIs feel modern. You write a function with type
hints, and FastAPI gives you - for free - request parsing, data validation, JSON serialization, and a
complete interactive API documentation page. That "for free" isn't marketing: it falls directly out of
one clever idea, which is that your **Python type hints become the single source of truth** for how the
API behaves. Learn that idea and FastAPI stops being a bag of decorators and becomes predictable.

This guide builds the mental model first the whole way: why type hints drive everything, what Pydantic is
doing under each model, how the `Depends()` system turns dependency injection into plain functions, and
when `async` actually helps (and when it quietly hurts). By the end you'll build a real, validated,
authenticated, tested API and understand every layer.

> 📝 This teaches the **framework**. It assumes you know **Python** - functions, classes, and especially
> **type hints** ([Python From Zero](/guides/python-from-zero) covers them; FastAPI leans on them hard).
> Helpful background: [REST APIs Explained](/guides/rest-apis-explained) and
> [What a Framework Even Is](/guides/what-a-framework-even-is).

## How to read this

Read in order - it builds one API (a small book service) and adds a layer per phase. Many pure-Python
snippets here are **runnable right on the page** (Pydantic models, validation); FastAPI app code that
needs a running server is shown with the commands to run it yourself. Phases carry difficulty badges.

## The phases

**Part 1 - The core (🟢 Basic)**
1. **[What FastAPI Is & Your First App](01-what-fastapi-is.md)** 🟢 - ASGI, your first endpoint, `uvicorn`, and the automatic interactive docs.
2. **[Path Operations & Parameters](02-path-operations-and-parameters.md)** 🟢 - routes, path and query parameters, and how type hints parse and validate them.
3. **[Pydantic Models & Validation](03-pydantic-models-and-validation.md)** 🟢 - request bodies as models, and validation that comes free from your types.

**Part 2 - A real application (🟡 Intermediate)**
4. **[Response Models & Status Codes](04-response-models-and-status-codes.md)** 🟡 - shaping output, hiding internal fields, and correct HTTP statuses.
5. **[Dependency Injection with Depends()](05-dependency-injection.md)** 🟡 - reusable dependencies for auth, DB sessions, and shared logic.
6. **[Async & Concurrency](06-async-and-concurrency.md)** 🟡 - `async def` vs `def`, when async helps, and the trap that blocks the event loop.
7. **[Databases with SQLModel](07-databases-with-sqlmodel.md)** 🟡 - persistence, sessions via dependencies, and CRUD.

**Part 3 - Production (🔴 Advanced → 🟢)**
8. **[Authentication & Security](08-authentication-and-security.md)** 🔴 - OAuth2 password flow, JWT, and security as dependencies.
9. **[Testing & Project Structure](09-testing-and-project-structure.md)** 🟡 - `TestClient`, pytest, and structuring a real project with routers.
10. **[Production & Where to Go Next](10-where-to-go-next.md)** 🟢 - deployment, background tasks, async pitfalls, and what to build.

> The whole framework rests on one idea: **types are the contract.** Once you see that, validation, docs,
> serialization, and DI all stop being separate features and become one coherent thing.


---

# What FastAPI Is & Your First App

You know Python and have maybe touched [REST APIs](/guides/rest-apis-explained). Now you want to
*serve* something over HTTP - a little book API that other programs can call. This guide covers the
framework that's become the default for new Python API projects: **FastAPI**.

Hold this in your head before any code: FastAPI's whole personality comes from one decision - 
**your Python type hints are the source of truth.** Annotate a function with the types it expects, and
FastAPI reuses those same annotations to parse incoming requests, validate them, generate documentation,
and serialize what you send back. One thing you write; four jobs it does.

## What FastAPI actually is

📝 **FastAPI** - a modern, asynchronous Python web framework for building APIs. It stands on two
well-tested libraries: **Starlette** handles the web machinery (routing, requests, responses), and
**Pydantic** handles data validation. FastAPI wires them together around your type hints.

It speaks a protocol called **ASGI**, which explains the "async" in the name and the "fast" in the
brand.

📝 **WSGI vs ASGI** - WSGI (Web Server Gateway Interface) is the older Python standard: one request,
one worker, blocked until the work is done. ASGI (the *A* is for **Asynchronous**) is the modern
successor - it can handle a request, hit a slow database or external API, and *while waiting* go serve
other requests instead of sitting idle. Same idea as `async`/`await`, applied to a whole web server.

💡 If you've read [Python's async chapter](/guides/python-from-zero), this is where it pays off:
FastAPI lets your handlers be `async def`, so an app waiting on I/O can stay busy. Plain `def` handlers
work too - FastAPI runs them safely in a threadpool. You don't have to go async on day one.

## Install it and write your first app

One install. The `[standard]` extra pulls in the server and a few niceties you'll want immediately:

```bash
pip install "fastapi[standard]"
```

*What just happened:* you installed FastAPI plus its recommended companions - most importantly
**Uvicorn**, the ASGI server that listens on a port and runs your app. Quote the string; some shells
treat `[` and `]` as special characters otherwise.

Now the smallest app that does something. Create a file called `main.py`:

```python
from fastapi import FastAPI

app = FastAPI()

@app.get("/")
def read_root():
    return {"message": "The book API is alive"}
```

*What just happened:* `app = FastAPI()` is your application object - the thing the server runs. The
`@app.get("/")` decorator says "when someone sends a `GET` request to the path `/`, run the function
below." Your function returns a plain Python `dict`, and FastAPI turns it into a JSON response - no
JSON library calls, no manual `Content-Type` header.

⚠️ Running `python main.py` won't start a server - this is app code, not a runnable script. FastAPI
apps need an ASGI server to run them. That's the next step.

Start the server with Uvicorn, pointing it at `main:app` (the `app` object inside `main.py`):

```bash
uvicorn main:app --reload
```

`--reload` restarts the server on every save. (FastAPI also ships `fastapi dev main.py`, a shortcut
that does the same thing with reload on by default - use whichever you like.)

```console
$ uvicorn main:app --reload
INFO:     Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)
INFO:     Started reloader process using WatchFiles
INFO:     Application startup complete.
```

*What just happened:* Uvicorn bound to `127.0.0.1:8000` and is waiting for requests. Open
`http://127.0.0.1:8000/` in a browser, or hit it from the terminal:

```http
GET http://127.0.0.1:8000/
```

```json
{
  "message": "The book API is alive"
}
```

*What just happened:* the request matched the `GET /` route, FastAPI called `read_root()`, and
serialized the returned dict to JSON. A working API in five lines of real code.

## The wow moment: docs you didn't write

Here's the feature that makes people switch frameworks. With the server still running, visit:

```http
GET http://127.0.0.1:8000/docs
```

You'll see **Swagger UI** - a full, interactive documentation page listing every endpoint, with a
"Try it out" button that sends real requests to your running app. A second flavour lives at `/redoc`
(ReDoc) if you prefer a cleaner reading layout. Zero lines of documentation written to get either.

💡 **Both pages are generated from a single machine-readable document FastAPI builds automatically: the
OpenAPI schema** (served at `/openapi.json`). OpenAPI is the industry-standard way to describe an API
 - what paths exist, what they accept, what they return. Tools across the ecosystem read it to
generate client libraries, run tests, or import the API into other software.

FastAPI produces all this from nothing because your type hints already told it everything it needed:
the paths come from your decorators, and (next phase) the parameters and response shapes come from the
types you annotate. **Your code already is the spec** - FastAPI just reads it.

## Type hints as the source of truth

This is the mental model that makes FastAPI *FastAPI*.

📝 **The core idea:** you annotate your function with Python types, and FastAPI uses those annotations
for four jobs at once - **parsing** incoming data into the right types, **validating** it (rejecting
bad input with a clear error), **documenting** it (in those auto-generated docs), and **serializing**
your return value back out. One declaration, kept consistent in four places.

```mermaid
flowchart LR
  T[Your type hints] --> V[Validation]
  T --> D[Auto docs / OpenAPI]
  T --> S[Serialization]
```

The payoff: these things can't drift apart. In a framework where you write validation by hand *and*
write docs by hand, the two slowly disagree - the docs say one thing, the code does another. With
FastAPI there's a single source, so validation rules, docs, and your editor's autocomplete all describe
the same truth.

You can feel this in pure Python, no framework involved - a function whose type hints describe exactly
what it takes and gives back:

```python runnable
def describe_book(title: str, year: int, price: float) -> str:
    return f"{title} ({year}) - ${price:.2f}"

# The hints document the function: title is text, year is a whole number, price is a decimal.
print(describe_book("Dune", 1965, 9.99))
print(describe_book.__annotations__)   # the hints are real data Python can read
```

```console
Dune (1965) - $9.99
{'title': <class 'str'>, 'year': <class 'int'>, 'price': <class 'float'>, 'return': <class 'str'>}
```

*What just happened:* the type hints (`title: str`, `year: int`, `price: float`) aren't decoration - 
they're stored on the function in `__annotations__`, readable by any program. That's the lever FastAPI
pulls: it inspects those same annotations on your handlers and turns them into validation rules and
documentation automatically. Next phase you'll annotate a real `GET /books/{id}` handler and watch
FastAPI parse and check the `id` for you, purely from the hint.

## Where FastAPI fits

FastAPI isn't the only Python framework, and it isn't always the right one:

- **Flask** - minimal and sync-first. It hands you routing and not much else; you bring your own
  validation, serialization, and docs. Great for tiny apps and total control, more manual work as the
  API grows.
- **Django** - batteries included. A full stack with an ORM, admin panel, templating, auth - built for
  large database-backed websites. Powerful, but a lot of machine for a focused JSON API.
- **FastAPI** - purpose-built for **modern APIs**: async-capable, with validation and interactive docs
  baked in *because* of the type hints. The sweet spot when you're building an API (rather than a
  server-rendered website) and want correctness and documentation without hand-rolling them.

💡 If you're not sure what "REST API" means - what `GET`, `POST`, paths, and status codes are - read
[REST APIs explained](/guides/rest-apis-explained) alongside this guide; this guide leans on those terms
from the next phase on.

Next phase is where the type-hint magic becomes concrete: **path operations and parameters** - how
`GET /books/{id}?year=1965` maps to a function with typed arguments that FastAPI parses and validates
for you.

## Recap

1. **FastAPI** is a modern, async Python web framework for building APIs, built on **Starlette** (web)
   + **Pydantic** (validation), speaking **ASGI**.
2. **ASGI** is the async successor to WSGI: a server that can keep serving other requests while one
   waits on slow I/O - the foundation for FastAPI's `async def` handlers.
3. A first app is tiny: `app = FastAPI()`, a `@app.get("/")` function returning a dict, run with
   `uvicorn main:app --reload` (or `fastapi dev`).
4. The standout feature is **automatic interactive docs** - Swagger UI at `/docs`, ReDoc at `/redoc`,
   and an **OpenAPI** schema at `/openapi.json` - all generated from your code, no doc-writing.
5. **Type hints are the single source of truth:** FastAPI reads your annotations to parse, validate,
   document, and serialize - so those four things never drift apart.
6. FastAPI's niche is **modern APIs**: lighter than Django, less manual than Flask, with validation and
   docs built in.

## Quick check

Three questions on the ideas that have to stick - what FastAPI is, where its docs come from, and the
type-hint model:

```quiz
[
  {
    "q": "What is the single design idea that drives most of FastAPI's features?",
    "choices": [
      "Your Python type hints are the source of truth - used to parse, validate, document, and serialize",
      "It compiles your Python to C for speed",
      "It generates a separate documentation file you maintain by hand",
      "It replaces HTTP with a faster custom protocol"
    ],
    "answer": 0,
    "explain": "FastAPI reads your type annotations and reuses them for validation, the OpenAPI docs, and serialization - one declaration kept consistent across all of them."
  },
  {
    "q": "You start your app with `uvicorn main:app --reload` and visit `/docs`. Where did that interactive documentation page come from?",
    "choices": [
      "FastAPI generated it automatically from your code's OpenAPI schema",
      "You have to write the HTML for it yourself before it appears",
      "Uvicorn ships a generic docs page unrelated to your app",
      "It only appears after you deploy to production"
    ],
    "answer": 0,
    "explain": "FastAPI builds an OpenAPI schema from your routes and type hints, and serves Swagger UI (/docs) and ReDoc (/redoc) from it automatically - no doc-writing required."
  },
  {
    "q": "What does ASGI give FastAPI that the older WSGI standard does not?",
    "choices": [
      "The ability to handle other requests while one is waiting on slow I/O (async support)",
      "Automatic conversion of Python to machine code",
      "A built-in database and admin panel",
      "Encryption of all responses by default"
    ],
    "answer": 0,
    "explain": "ASGI is the asynchronous successor to WSGI. It lets the server keep serving requests while one awaits slow I/O, which is what makes FastAPI's async def handlers worthwhile."
  }
]
```


---

# Path Operations & Parameters

Phase 1 got an app running: write a typed function, get a validated, documented endpoint. This phase
zooms in on the routes themselves - how you say "this function handles `GET /books`," how you grab the
`42` out of `/books/42`, how you read `?limit=10` off the URL, and how a single `int` annotation turns
into a validation rule that rejects garbage before your code ever runs.

The mental model: **a route in FastAPI is just a normal Python function, and its signature is the
spec.** The decorator says *which* requests reach the function. The parameters - and their type hints - 
say *what those requests must look like*. You don't write parsing or validation; you describe the shape,
and FastAPI enforces it.

## Path operations - a route is a decorated function

📝 **Path operation** - FastAPI's name for a route. "Path" is the URL path (`/books`); "operation" is the
HTTP method (`GET`, `POST`, ...). Together they're "the function that runs for `GET /books`." You declare
one by decorating a function: the decorator *is* the method-plus-path.

```python
from fastapi import FastAPI

app = FastAPI()

@app.get("/books")
def list_books():
    return [{"id": 1, "title": "Dune"}]

@app.post("/books")
def create_book():
    return {"message": "a book would be created here"}
```

*What just happened:* `@app.get("/books")` registers `list_books` as the handler for `GET /books`.
`@app.post("/books")` registers a *different* function for `POST /books` - same path, different method,
so it's a separate path operation. The decorator name (`get`, `post`, `put`, `delete`, `patch`) maps
one-to-one onto the HTTP verb, matching the REST model from
[REST APIs Explained](/guides/rest-apis-explained): the URL names the resource, the method is the verb
you apply to it. Whatever you `return` becomes the JSON response body.

💡 You'll almost always use `get` (read), `post` (create), `put`/`patch` (update), and `delete` (remove).
There's a decorator per method; pick the one that matches what the request *does* to the resource.

## Path parameters - pulling values out of the URL

A `GET /books` lists every book. But `GET /books/42` should return *one* book - the one with id `42`.
That `42` changes per request, so you can't hard-code it into the route string. Mark it as a **path
parameter** with curly braces, then receive it as a function argument.

```python
@app.get("/books/{book_id}")
def get_book(book_id: int):
    return {"id": book_id, "title": "Dune"}
```

*What just happened:* `{book_id}` in the path is a placeholder. FastAPI matches `/books/42`, pulls out
`"42"`, and passes it to your function as the `book_id` argument. The name in the braces must match the
parameter name exactly - that's how FastAPI wires them together.

Look at the annotation: `book_id: int`. The value in a URL is always *text* - `"42"` is a string. That
`int` hint tells FastAPI to convert it to a real integer before handing it over, so inside your function
`book_id` is the number `42`, not the string `"42"`. If the conversion *fails* - say someone requests
`/books/banana` - FastAPI doesn't run your function at all. It returns a `422 Unprocessable Entity` with
a precise error:

```json
{
  "detail": [
    {
      "type": "int_parsing",
      "loc": ["path", "book_id"],
      "msg": "Input should be a valid integer, unable to parse string as an integer",
      "input": "banana"
    }
  ]
}
```

*What just happened:* `banana` can't be an `int`, so FastAPI rejected the request before your code saw
it. The error names exactly *where* it went wrong (`["path", "book_id"]`), *what* was expected, and
*what* it got - zero validation code written.

💡 This is the Phase 1 idea made concrete: **the type hint is doing validation, not just documentation.**
One word - `int` - bought you parsing, a guaranteed-correct type inside your function, and an automatic,
descriptive `422` for bad input. Change it to `str` and `/books/banana` would sail right through. The hint
*is* the rule.

## Query parameters - everything after the `?`

Path parameters identify *which* resource. **Query parameters** tune *how* you want it - filtering,
paging, sorting. They're the `?skip=0&limit=10` part of a URL. The rule for declaring them: **any
function parameter that isn't in the path becomes a query parameter.**

```python
@app.get("/books")
def list_books(skip: int = 0, limit: int = 10):
    return {"skip": skip, "limit": limit, "books": ["..."]}
```

*What just happened:* `skip` and `limit` don't appear in the path string `"/books"`, so FastAPI treats
them as query parameters read from the URL's query string. Because they have **default values** (`= 0`,
`= 10`), they're *optional* - a plain `GET /books` uses the defaults. They're typed `int`, so they get
the same parse-and-validate treatment as path parameters: `?limit=abc` earns a `422`.

You'd call it like this:

```http
GET /books?skip=20&limit=5 HTTP/1.1
Host: localhost:8000
```

Or from the terminal:

```bash
curl "http://localhost:8000/books?skip=20&limit=5"
```

```console
{"skip":20,"limit":5,"books":["..."]}
```

*What just happened:* FastAPI matched `?skip=20&limit=5` to your two parameters by name, converted both
to `int`, and passed them in. Leave them off (`curl http://localhost:8000/books`) and you'd get the
defaults, `skip=0` and `limit=10`. Default present means optional; default absent means required (and a
missing required query param is - you guessed it - a `422`).

## Optional & constrained parameters - richer rules than "is it an int"

Sometimes a query parameter is genuinely optional with *no* meaningful default - "filter by author, but
only if the caller asked." That's an `Optional` (or `| None`) annotation with a default of `None`:

```python
@app.get("/books")
def list_books(author: str | None = None):
    if author is None:
        return {"books": "all books"}
    return {"books": f"books by {author}"}
```

*What just happened:* `author: str | None = None` says "a string, or nothing." With no `?author=...` in
the URL, `author` is `None` and you return everything; with `?author=Herbert`, it's `"Herbert"`. The
`= None` default is what makes it optional - without a default, FastAPI would *require* it. (`str | None`
is the modern syntax; older code writes `Optional[str]` from `typing` - same thing.)

But "it's a string" is often too loose. You might want "no longer than 50 characters," or "an id that
must be at least 1." For that, FastAPI gives you `Query()` and `Path()` - helpers that attach extra
validation rules to a parameter:

```python
from fastapi import FastAPI, Query, Path

app = FastAPI()

@app.get("/books/{book_id}")
def get_book(
    book_id: int = Path(ge=1),
    q: str | None = Query(default=None, max_length=50),
):
    return {"book_id": book_id, "q": q}
```

*What just happened:* `Path(ge=1)` adds the rule "**g**reater than or **e**qual to 1" to the path
parameter - so `/books/0` is now invalid. `Query(default=None, max_length=50)` keeps `q` optional but
caps its length at 50 characters. These constraints join the same validation pass: violate one and you
get a `422` describing it, exactly like a type mismatch. Now request `/books/0`:

```json
{
  "detail": [
    {
      "type": "greater_than_equal",
      "loc": ["path", "book_id"],
      "msg": "Input should be greater than or equal to 1",
      "input": "0"
    }
  ]
}
```

*What just happened:* `0` parsed fine as an `int` but failed the `ge=1` rule, so FastAPI rejected it with
a `422` that names the broken constraint. ⚠️ Don't confuse the helper's argument with its job:
`Query(default=None, ...)` sets the default, while `max_length`/`ge`/`le`/`min_length`/`pattern` set the
*rules*. The default decides "required or optional"; the rules decide "what counts as valid."

## How it works + the ordering gotcha

Notice what you never did: parse a string, write an `if not isinstance(...)`, hand-build an error
response. You *described* each parameter with a type and maybe a constraint, and FastAPI did the rest.

💡 Here's the machinery. **At startup, FastAPI inspects every path-operation function's signature** - 
reading the parameter names, their type hints, and any `Query`/`Path` rules - and builds a schema for
the route. That one schema powers three things at once: **parsing** the incoming request, **validating**
it (generating the `422` when it fails), and **documenting** it in the auto-generated interactive docs
from Phase 1. The signature is the single source of truth, so the docs are always in sync with the
actual behavior - built from the same description.

⚠️ **The ordering gotcha.** FastAPI matches routes **in the order you declare them**, top to bottom, and
stops at the first match. So a *fixed* path that looks like it could be a path parameter must come
**first**. Imagine you want `GET /books/featured` to return a curated list:

```python
@app.get("/books/{book_id}")
def get_book(book_id: int):
    return {"id": book_id}

@app.get("/books/featured")          # unreachable!
def featured_books():
    return {"books": "the good ones"}
```

*What just happened:* This is broken. A request for `/books/featured` hits `@app.get("/books/{book_id}")`
*first*, because it's declared first and `/books/featured` matches the `{book_id}` pattern. FastAPI then
tries to parse `"featured"` as an `int`, fails, and returns a `422` - `featured_books` never runs. Fix it
by declaring the literal route **before** the dynamic one:

```python
@app.get("/books/featured")          # specific path first
def featured_books():
    return {"books": "the good ones"}

@app.get("/books/{book_id}")         # dynamic catch-all second
def get_book(book_id: int):
    return {"id": book_id}
```

*What just happened:* Now `/books/featured` matches the literal route and stops there, while
`/books/42` falls through to the parameterized one. Rule of thumb: **specific before general.**

So far every endpoint reads data from the *URL*. But creating a book needs a whole payload - title,
author, year, price - which doesn't belong in the path or query string. That's the request **body**,
where Pydantic models take over. Next phase.

## Recap

1. A **path operation** is a route: the decorator (`@app.get`, `@app.post`, ...) sets the HTTP method and
   the path, and the decorated function handles those requests.
2. **Path parameters** (`/books/{book_id}` + `book_id: int`) pull a value out of the URL; the type hint
   parses it *and* validates it, returning an automatic `422` for bad input.
3. **Query parameters** are any function parameter not in the path. A default value makes them optional
   (`limit: int = 10`); no default makes them required.
4. For optional-with-no-default use `str | None = None`; for richer rules use `Query(...)` / `Path(...)`
   with constraints like `max_length` and `ge`, which produce constraint-specific `422`s.
5. 💡 FastAPI reads each function's signature **at startup** and builds one schema that drives parsing,
   validation, and the auto docs - so the docs can never drift from the behavior.
6. ⚠️ Routes match **in declaration order**: put literal paths (`/books/featured`) **before** dynamic ones
   (`/books/{book_id}`), or the dynamic route swallows them.

You can now route requests and validate everything that arrives in the URL. Next: data that arrives in
the request *body* - with Pydantic models doing the same type-driven validation, for whole objects.

## Quick check

Make sure the core idea stuck - that the signature drives everything:

```quiz
[
  {
    "q": "In `@app.get(\"/books/{book_id}\")` with `def get_book(book_id: int):`, what happens when a client requests `/books/banana`?",
    "choices": [
      "FastAPI returns a 422 error and never runs get_book, because \"banana\" can't be parsed as an int",
      "get_book runs with book_id set to the string \"banana\"",
      "FastAPI silently converts it to 0 and runs the function",
      "The server crashes with an unhandled exception"
    ],
    "answer": 0,
    "explain": "The `int` hint is a validation rule. \"banana\" can't become an int, so FastAPI rejects the request with a descriptive 422 before your function is ever called."
  },
  {
    "q": "How do you make a function parameter a *query* parameter rather than a path parameter?",
    "choices": [
      "Include it as a function argument but NOT in the path string - anything not in the path becomes a query parameter",
      "Wrap it in curly braces in the path string",
      "Decorate it with @query above the function",
      "Give it the type hint `Query` instead of `int`"
    ],
    "answer": 0,
    "explain": "Path parameters appear in the path with `{braces}`. Any other function parameter is read from the query string. A default value (like `limit: int = 10`) makes it optional."
  },
  {
    "q": "You declare `@app.get(\"/books/{book_id}\")` first and `@app.get(\"/books/featured\")` second. What goes wrong?",
    "choices": [
      "`/books/featured` matches the {book_id} route first, FastAPI tries to parse \"featured\" as an int, and returns a 422 - the featured route never runs",
      "Both routes work fine; FastAPI picks the more specific one automatically",
      "FastAPI refuses to start because of the conflict",
      "`/books/featured` returns featured books, but `/books/42` breaks"
    ],
    "answer": 0,
    "explain": "Routes match in declaration order. The dynamic `/books/{book_id}` is declared first, so it captures `/books/featured` before the literal route is ever considered. Declare literal paths before dynamic ones."
  }
]
```


---

# Pydantic Models & Validation

[Phase 2](02-path-operations-and-parameters.md) showed a type hint on a path or query parameter quietly
doing real work: FastAPI reads the hint and parses, validates, and converts the value for you. That
trick has a name, and it's a whole library - **Pydantic**. Path and query parameters are the small
version; the full power shows up when a client sends a JSON *body* you need to trust before you touch it.

The mental model for this phase: a Pydantic model is a **typed gate**. You describe the shape of the
data once - a class with typed fields - and Pydantic stands at the door, checking every piece of data
that tries to come in. Good data passes through as a clean, typed Python object. Bad data gets turned
away with a precise error. You stop writing `if not isinstance(...)` checks by hand; the shape *is* the
check.

## What Pydantic actually is

📝 **Pydantic** is a data-validation library. You define a class that extends `BaseModel`, give it typed
fields, and Pydantic validates and coerces any data you build it from against those types - **at
runtime**. This is the crucial difference from the [type hints](/guides/python-from-zero) you met in
Python: a plain hint like `age: int` is a note for humans and tools that the interpreter ignores while
running. Pydantic *enforces* the same hint when the object is constructed.

**Pydantic is separate from FastAPI.** It's its own library, usable in any Python program - config
loading, parsing files, cleaning data. FastAPI just leans on it hard: every request body you'll define
is a Pydantic model. That means the examples below are **pure Python and run on this page** - no
server, no `uvicorn` - so you can watch validation succeed and fail, live.

> 💡 **Key point.** A Pydantic model is the same *describe-the-fields* idea as a dataclass
> ([Python From Zero, Phase 15](/guides/python-from-zero)), with one decisive addition: it **checks and
> converts the data** at construction time instead of trusting it. Dataclass for data you already trust;
> Pydantic at the boundary where untrusted data arrives.

## Your first model - and watch it reject bad data

Model a `Book` for our book service: a title, an author, a year, and a price. Extending `BaseModel` and
listing typed fields is the whole definition. This block runs - build a book from a dict, print it, then
feed it garbage and see what Pydantic does.

```python runnable
from pydantic import BaseModel, ValidationError

class Book(BaseModel):
    title: str
    author: str
    year: int
    price: float

# Good data - Pydantic builds a clean, typed object:
data = {"title": "Dune", "author": "Frank Herbert", "year": 1965, "price": 14.99}
book = Book(**data)
print(book)
print(book.title, "costs", book.price)

# Bad data - year isn't a number, price is missing entirely:
try:
    Book(title="Bad Book", author="Nobody", year="not-a-year")
except ValidationError as e:
    print(e)
```
*What just happened:* the first `Book(**data)` sailed through - Pydantic checked each field against its
type and handed you a real `Book` object with `.title`, `.author`, `.year`, and `.price` attributes. The
second attempt raised a `ValidationError`, and it's specific: it tells you `year` couldn't be parsed as
an integer **and** that `price` is required but missing - both problems, in one report, pointing at the
exact fields. You didn't write a single validation check. The class *is* the validation.

Contrast that with a plain dataclass, which trusts whatever you give it:

```python runnable
from dataclasses import dataclass

@dataclass
class Book:
    title: str
    author: str
    year: int
    price: float

# The dataclass happily stores nonsense - the `: int` hint is never enforced:
book = Book(title="Junk", author="?", year="not-a-year", price="free")
print(book)
print(type(book.year))   # it's a str, not an int - the bug is now inside your object
```
*What just happened:* the dataclass accepted `year="not-a-year"` and `price="free"` without complaint
and stored them as strings. The `: int` and `: float` hints were ignored at runtime, as Python type hints
always are. The bad data now sits *inside* your object, waiting to blow up later when you try arithmetic
on a string. That's the gap Pydantic closes: it moves the failure to the *boundary*, where it's cheap to
diagnose, instead of letting it leak deep into your code.

## Field constraints - rules that live with the type

Type-correct isn't the same as *valid*. A price of `-5.0` is a perfectly good `float` and a perfectly
absurd price. A year of `99` parses as an `int` but no book was printed then. Pydantic lets you attach
**constraints** to a field with `Field(...)`, so the rule lives right next to the type it guards.

```python runnable
from pydantic import BaseModel, Field, ValidationError

class Book(BaseModel):
    title: str = Field(min_length=1)         # no empty titles
    author: str = Field(min_length=1)
    year: int = Field(ge=1450, le=2100)      # between 1450 and 2100 inclusive
    price: float = Field(gt=0)               # strictly greater than 0

# Valid - every constraint satisfied:
good = Book(title="Dune", author="Frank Herbert", year=1965, price=14.99)
print("OK:", good)

# Invalid - empty title, year too early, price not positive:
try:
    Book(title="", author="Frank Herbert", year=1200, price=0)
except ValidationError as e:
    print(e)
```
*What just happened:* the valid book passed because it cleared every rule. The invalid one tripped three
constraints at once - `title` was empty (`min_length=1`), `year` of `1200` fell below `ge=1450`, and
`price` of `0` failed `gt=0` (greater than, not greater-or-equal) - and Pydantic reported all three with
the limits it expected. `gt` is "greater than," `ge` is "greater than or equal," `le` is "less than or
equal" (and `lt` exists too); `min_length` works on strings and lists.

💡 This is **declarative validation**: you *declare* what valid looks like as part of the field, and
Pydantic figures out *how* to check it. The rule and the data it protects never drift apart - change the
field, the constraint moves with it. Compare that to scattering hand-written `if price <= 0: raise ...`
checks across every function that touches a book.

## Using a model as a request body

Now the payoff for FastAPI. In [Phase 2](02-path-operations-and-parameters.md), a parameter typed as a
simple type (`int`, `str`) became a path or query parameter. The rule that completes the picture: **when
you type a parameter as a Pydantic model, FastAPI reads it from the JSON request body.** It pulls the raw
JSON, hands it to your model for validation, and - if it passes - gives your function a fully typed
object. If it fails, FastAPI never even calls your function; it returns a `422 Unprocessable Entity`
automatically, with the same precise error detail you saw above.

This endpoint code needs a running server, so it's shown as plain Python (run it yourself with the commands
from Phase 1):

```python
from fastapi import FastAPI
from pydantic import BaseModel, Field

app = FastAPI()

class Book(BaseModel):
    title: str = Field(min_length=1)
    author: str = Field(min_length=1)
    year: int = Field(ge=1450, le=2100)
    price: float = Field(gt=0)

@app.post("/books")
def create_book(book: Book):        # typed as the model → comes from the JSON body
    # `book` is already validated. No checks needed here.
    return {"message": f"Added {book.title} by {book.author}", "price": book.price}
```
*What just happened:* the single line `book: Book` did everything. FastAPI saw a parameter typed as a
`BaseModel`, so it knew to read the request body, validate it against `Book`, and pass you a ready-to-use
object. Inside `create_book` there are **zero** validation checks - by the time your code runs, the data
is guaranteed valid. The gate is at the door, not scattered through the house.

A valid request - this JSON body sails through and your function runs:

```json
{
  "title": "Dune",
  "author": "Frank Herbert",
  "year": 1965,
  "price": 14.99
}
```

An invalid one - `year` is below the allowed range and `price` isn't positive - never reaches your function.
FastAPI returns `422` with a body like this:

```json
{
  "detail": [
    {
      "type": "greater_than_equal",
      "loc": ["body", "year"],
      "msg": "Input should be greater than or equal to 1450",
      "input": 1200
    },
    {
      "type": "greater_than",
      "loc": ["body", "price"],
      "msg": "Input should be greater than 0",
      "input": 0
    }
  ]
}
```
*What just happened:* FastAPI turned your model's `ValidationError` into a clean HTTP `422` response.
Each entry in `detail` points at the offending field via `loc` (`["body", "year"]` means "the `year`
field in the request body"), explains what was expected, and echoes the bad `input`. The client gets a
genuinely useful error, and you wrote none of it - it fell straight out of the model definition.

## Coercion, optionals, and nesting

A few behaviors round out the mental model.

📝 **Coercion.** Pydantic doesn't just check types - it *converts* compatible ones. Hand it the string
`"2020"` for an `int` field and it gives you the integer `2020`. This is why JSON works smoothly: numbers
arriving as strings get tidied up. But it only coerces what's *sensibly* convertible - `"not-a-year"` has
no integer meaning, so it's rejected rather than guessed at.

```python runnable
from pydantic import BaseModel

class Book(BaseModel):
    title: str
    year: int
    price: float

# Strings that *look* like numbers get coerced to the declared type:
book = Book(title="Dune", year="1965", price="14.99")
print(book)
print(type(book.year), type(book.price))   # int and float - converted, not stored as str
```
*What just happened:* you passed `year` and `price` as strings, and Pydantic coerced them to a real `int`
and `float` because those strings have an unambiguous numeric meaning. The printed types confirm the
conversion. Try changing `"1965"` to `"nineteen"` and you'll get a `ValidationError` instead - coercion
has limits, and gibberish hits them.

⚠️ **Coercion can surprise you.** Lax coercion is convenient but occasionally too generous - depending on
configuration, things like `"1"` might slip into a `bool`, or a float might be quietly truncated. If you
need exact-type-only behavior (no string-to-int favors), Pydantic offers **strict mode** to turn coercion
off per-field or per-model. The default is *lax* and helpful; strict mode exists for when it isn't.

**Optional fields with defaults.** Give a field a default value and it becomes optional - callers can leave
it out. Use `X | None = None` for "might genuinely be absent."

```python runnable
from pydantic import BaseModel

class Book(BaseModel):
    title: str
    author: str
    in_stock: bool = True            # optional: defaults to True if omitted
    discount: float | None = None    # optional and nullable

print(Book(title="Dune", author="Frank Herbert"))
print(Book(title="Dune", author="Frank Herbert", in_stock=False, discount=2.50))
```
*What just happened:* `in_stock` and `discount` both have defaults, so the first `Book(...)` - supplying
only `title` and `author` - is completely valid; Pydantic filled in `in_stock=True` and `discount=None`.
The second call overrode both. Required fields are the ones *without* a default.

**Nesting.** A model field can be typed as *another model*. Pydantic validates the whole tree - outer object,
inner object, all the way down.

```python runnable
from pydantic import BaseModel

class Author(BaseModel):
    name: str
    country: str

class Book(BaseModel):
    title: str
    author: Author          # a field whose type is another model
    year: int

data = {
    "title": "Dune",
    "author": {"name": "Frank Herbert", "country": "USA"},
    "year": 1965,
}
book = Book(**data)
print(book)
print(book.author.name)     # nested object, fully typed
```
*What just happened:* `author: Author` told Pydantic the `author` field is itself a model, so it
validated the nested `{"name": ..., "country": ...}` dict against `Author` and gave you `book.author` as
a real `Author` object - hence `book.author.name` works with full typing. Mistype anything inside the
nested dict and you'd get a `ValidationError` pointing at the nested path, like `["author", "country"]`.

💡 **The payoff, stated plainly.** Define the shape once as a model, and *everything* follows from it:
validation (this phase), automatic `422` errors, the interactive docs that show the exact schema, and - 
next phase - serialization of your *responses*. One definition, many free features: **types are the
contract.**

## Recap

1. **Pydantic is a runtime data-validation library** - define a class extending `BaseModel` with typed
   fields, and it validates and coerces data against those types when the object is built. It's separate from
   FastAPI and pure Python, so its examples run anywhere.
2. **A `ValidationError` is precise** - it names every bad field at once, says what was expected, and (in
   FastAPI) becomes an automatic `422` with the same detail.
3. **Constraints live with the field** via `Field(...)`: `gt`/`ge`/`lt`/`le` for numbers, `min_length` for
   strings and lists. Declarative - the rule never drifts from the data it guards.
4. **A parameter typed as a model = the request body.** FastAPI reads the JSON, validates it against the
   model, and hands your function a clean typed object - or returns `422` and never calls you.
5. **Coercion converts compatible types** (`"2020"` → `2020`) but rejects nonsense; the default is lax, and
   strict mode exists when you need exact types.
6. **Optional fields** get defaults (`in_stock: bool = True`, or `X | None = None`); **nested models** let
   one model contain another, validated all the way down.

Next phase flips the direction: instead of validating data coming *in*, use models to shape and control
data going *out* - response models, hidden fields, and correct status codes.

## Quick check

Three questions on the ideas that have to stick - what Pydantic enforces, where a model body comes from, and
what coercion does.

```quiz
[
  {
    "q": "You define `class Book(BaseModel)` with `price: float` and call `Book(title=\"X\", author=\"Y\", year=2000, price=\"oops\")`. What happens?",
    "choices": [
      "It builds the object and stores \"oops\" as the price",
      "It raises a ValidationError - \"oops\" can't be coerced to a float",
      "Python raises a TypeError before Pydantic sees it",
      "It silently sets price to 0.0"
    ],
    "answer": 1,
    "explain": "Unlike a plain dataclass (which would store the string), Pydantic enforces the type at construction. \"oops\" has no sensible float meaning, so coercion fails and a ValidationError is raised - pointing at the price field."
  },
  {
    "q": "In FastAPI, what makes a function parameter come from the JSON request body rather than the path or query string?",
    "choices": [
      "Naming the parameter `body`",
      "Adding `@app.post` instead of `@app.get`",
      "Typing the parameter as a Pydantic BaseModel",
      "Wrapping it in `Body(...)` - there's no other way"
    ],
    "answer": 2,
    "explain": "FastAPI's rule: a parameter typed as a Pydantic model is read from the request body, validated against the model, and passed in as a typed object (or it auto-returns 422). The HTTP method and parameter name don't determine this."
  },
  {
    "q": "A field is declared `year: int`. A client sends the JSON value `\"1965\"` (a string). With Pydantic's default behavior, what does your object's `year` end up as?",
    "choices": [
      "The string \"1965\" - Pydantic never changes types",
      "A ValidationError, because a string isn't an int",
      "The integer 1965 - Pydantic coerces compatible types",
      "None, because the value didn't match exactly"
    ],
    "answer": 2,
    "explain": "By default Pydantic is lax: it coerces sensibly-convertible values, so the string \"1965\" becomes the integer 1965. (Gibberish like \"nineteen\" would still raise. Strict mode exists if you want to forbid the conversion.)"
  }
]
```


---

# Response Models & Status Codes

Phase 3 used Pydantic models to describe what comes *in* - the request body - and FastAPI validated it
for free. This phase is the mirror image: describing what goes *out*. The mental model: **the shape of
what you send is not the same as the shape of what you return, and pretending they're the same is the
single most common way APIs leak data or accept things they shouldn't.**

Think of an endpoint as having two contracts. The **input contract** is what a client is allowed to send
you (a new book's title and author - but not its database id, and definitely not your private notes
about it). The **output contract** is what you promise to hand back (the id you assigned, the public
fields - but again, not your private notes). Those two contracts are *different*, so they deserve
*different models* - this phase is FastAPI's clean way to declare both.

## `response_model` - declaring the shape of what you return

📝 **`response_model`** - a parameter you pass to a path operation that tells FastAPI the Pydantic model
your endpoint's return value should conform to. FastAPI then does three things with it: validates that your
return value fits the shape, **serializes** it to JSON in exactly that shape, and **documents** it in the
auto-generated `/docs` page. One declaration, three jobs.

Here's the Book domain from Phase 3, now with an output model declared on the endpoint:

```python
from fastapi import FastAPI
from pydantic import BaseModel

app = FastAPI()

class BookPublic(BaseModel):
    id: int
    title: str
    author: str

@app.get("/books/{book_id}", response_model=BookPublic)
def get_book(book_id: int):
    # Imagine this came from a database.
    return {"id": book_id, "title": "Dune", "author": "Frank Herbert"}
```

*What just happened:* the `response_model=BookPublic` on the decorator is the whole point. Even though
`get_book` returns a plain `dict`, FastAPI runs that dict *through* `BookPublic` on the way out - checking
the fields exist and have the right types, then producing JSON shaped exactly like `BookPublic`. Your
function can return a dict, a Pydantic object, or an ORM row; the response model is what the client
actually sees. `/docs` now shows the precise response schema, so the documentation can't drift from
reality.

💡 The return type annotation (`def get_book(...) -> BookPublic:`) works too and is increasingly the
preferred style. `response_model=` is shown here because it's explicit and has a couple of extra powers
(like `response_model=None` to opt out). Pick one; don't use both on the same endpoint with conflicting types.

## Input vs output models - the key pattern

This is the idea the whole phase is built around. You want two models:

- **`BookCreate`** - what a client *sends* to create a book. No `id` (the server assigns that), no internal
  fields.
- **`BookPublic`** - what you *return*. Has the `id`, has the public fields, but hides anything internal.

Why bother with two when one "Book" model would compile fine?

⚠️ Two reasons, both bugs waiting to happen if you ignore them. **First, a single model lets clients set
fields they have no business setting** - like the `id`, or an `is_admin` flag, or `created_by`. If your
input model has an `id` field, a client can pick its own id. **Second, returning your internal object
leaks fields** - a `secret_notes` column, a password hash, an internal cost. The response model is your
filter: it strips the output down to exactly the fields it declares, no matter what extra junk the source
object carries.

Prove the stripping with a runnable example. Pydantic does the filtering, so this works without a
running server - exactly what FastAPI does internally with your return value:

```python runnable
from pydantic import BaseModel

# What clients send - notice: no id, no internal fields.
class BookCreate(BaseModel):
    title: str
    author: str

# What we store internally - has server-controlled and private fields.
class BookInDB(BaseModel):
    id: int
    title: str
    author: str
    secret_notes: str        # internal! must never reach the client
    acquisition_cost: float  # also internal

# What we return to clients - public fields only.
class BookPublic(BaseModel):
    id: int
    title: str
    author: str

# Simulate the full round trip.
incoming = BookCreate(title="Dune", author="Frank Herbert")

stored = BookInDB(
    id=1,
    title=incoming.title,
    author=incoming.author,
    secret_notes="bought cheap at an estate sale",
    acquisition_cost=2.50,
)

# This is what response_model=BookPublic does: filter the internal object
# down to exactly the public model's fields.
public = BookPublic.model_validate(stored.model_dump())

print("Stored object has secrets:", stored.model_dump())
print("Public response is clean: ", public.model_dump())
```
```console
Stored object has secrets: {'id': 1, 'title': 'Dune', 'author': 'Frank Herbert', 'secret_notes': 'bought cheap at an estate sale', 'acquisition_cost': 2.5}
Public response is clean:  {'id': 1, 'title': 'Dune', 'author': 'Frank Herbert'}
```

*What just happened:* the internal `BookInDB` object carries `secret_notes` and `acquisition_cost`. Fed
through `BookPublic`, those fields vanished - `BookPublic` only knows about `id`, `title`, and `author`,
so that's all that survives. In a real FastAPI app you don't write the `model_validate` line yourself;
declaring `response_model=BookPublic` makes FastAPI do precisely this filtering on every response. The
client *cannot* see what the output model doesn't declare.

The same split wired into real endpoints - `BookCreate` going in, `BookPublic` coming out:

```python
from fastapi import FastAPI
from pydantic import BaseModel

app = FastAPI()

class BookCreate(BaseModel):
    title: str
    author: str

class BookPublic(BaseModel):
    id: int
    title: str
    author: str

@app.post("/books", response_model=BookPublic)
def create_book(book: BookCreate):
    # The server assigns the id - the client never gets to.
    new_id = 1  # pretend the database generated this
    return {"id": new_id, "title": book.title, "author": book.author}
```

*What just happened:* the request body is parsed as `BookCreate`, which has no `id` - so there's no way
for a client to smuggle one in; the server is fully in control of that. The response is shaped by
`BookPublic`, which *does* include `id`. Two models making the input and output contracts explicit is the
entire pattern - you'll see it everywhere in well-built FastAPI code.

## Status codes - saying what actually happened

An HTTP response isn't just a body; it carries a **status code** that tells the client what happened in
one number. By default FastAPI returns `200 OK` from every successful endpoint, but `200` isn't always
the right answer. A freshly created resource deserves `201`. A successful delete with nothing to return
deserves `204`. Using the right code is part of a clean API contract - clients (and other tools) read
these codes to decide what to do next.

The ones you'll reach for constantly:

| Code | Means | Use it when |
|------|-------|-------------|
| `200 OK` | Success, here's the body | A normal `GET` or update returning data |
| `201 Created` | A new resource was created | A `POST` that creates something |
| `204 No Content` | Success, deliberately no body | A `DELETE` that succeeded |
| `404 Not Found` | The thing you asked for doesn't exist | Looking up a book id that isn't there |
| `422 Unprocessable Entity` | The request body failed validation | FastAPI returns this for you automatically when Pydantic validation fails |

For the full tour of what each status code family means and why, see
[HTTP Explained](/guides/http-explained) - it covers the 2xx/4xx/5xx logic that this table only summarizes.

You set the success code with `status_code` on the decorator. A `POST` that creates a book should say so:

```python
from fastapi import FastAPI, status
from pydantic import BaseModel

app = FastAPI()

class BookCreate(BaseModel):
    title: str
    author: str

class BookPublic(BaseModel):
    id: int
    title: str
    author: str

@app.post("/books", response_model=BookPublic, status_code=status.HTTP_201_CREATED)
def create_book(book: BookCreate):
    new_id = 1
    return {"id": new_id, "title": book.title, "author": book.author}
```

*What just happened:* `status_code=status.HTTP_201_CREATED` (just the integer `201` with a readable name)
makes a successful create respond with `201 Created` instead of the default `200`. You could write
`status_code=201` directly; the `status` constants exist so your code reads as intent, not magic numbers.
The `/docs` page picks this up too, so the documented success code matches what the endpoint really sends.

📝 The `422` is special: you almost never set it yourself. When a request body fails Pydantic validation
 - wrong type, missing required field - FastAPI automatically rejects it with `422` and a detailed JSON
explanation. That's the validation from Phase 3 showing up as an HTTP status.

## Raising errors with `HTTPException`

So far our endpoints assume the happy path. But what about looking up a book that doesn't exist? You
don't `return` an error - you **raise** one. FastAPI gives you `HTTPException` for exactly this: raise
it, and FastAPI catches it and turns it into a clean JSON error response with the status code you chose.

📝 **`HTTPException`** - an exception you `raise` to short-circuit a request with a specific HTTP status
and message. FastAPI converts it into a proper error response; you never build the response by hand.

```python
from fastapi import FastAPI, HTTPException, status
from pydantic import BaseModel

app = FastAPI()

class BookPublic(BaseModel):
    id: int
    title: str
    author: str

# Pretend this is our database.
books = {1: {"id": 1, "title": "Dune", "author": "Frank Herbert"}}

@app.get("/books/{book_id}", response_model=BookPublic)
def get_book(book_id: int):
    if book_id not in books:
        raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Book not found")
    return books[book_id]
```

*What just happened:* when the requested `book_id` isn't in our `books` store, we `raise
HTTPException(...)` with `404` and a `detail` message. FastAPI stops processing the request right there
and sends back a `404` response - it doesn't apply the `response_model`, because we never returned a
value. Raising (not returning) cleanly aborts the endpoint.

If a client requests `GET /books/999`, the response status is `404 Not Found` and the body is:

```json
{
  "detail": "Book not found"
}
```

*What just happened:* FastAPI wrapped your `detail` string in a consistent JSON shape - always a
`detail` key - and set the HTTP status to `404`. Every error in your API comes out in this same
predictable envelope: one place to look for what went wrong.

## Why this matters

💡 The input/output-model split is *the* FastAPI way to keep your API contract clean and safe. Separate
models mean clients can't set server-controlled fields (no rogue `id`s), and your internal objects can't
leak private fields (no `secret_notes` in the wild). Pair that with `response_model` and your `/docs`
page is always accurate - the documented response shape *is* the real response shape, generated from the
same source. Add correct status codes (`201` on create, a clean `404` via `HTTPException`) and your API
communicates clearly to every client and tool that talks to it.

⚠️ The classic beginner mistake is using **one model for everything** - a single `Book` class for input,
output, and storage. It feels simpler on day one and turns into a liability by week two: either you
expose fields you didn't mean to, or you accept fields you shouldn't, or both. Start with the split. As
the API grows you'll often add a third model for the database/internal shape (like `BookInDB` above) - 
three models, three contracts, zero leaks.

Next up: **dependency injection with `Depends()`** - how FastAPI lets you pull shared logic (database
sessions, the current user, common parameters) into reusable functions your endpoints ask for.

## Recap

1. **`response_model`** declares the shape an endpoint returns; FastAPI validates the return value against
   it, serializes to exactly that shape, and documents it in `/docs`.
2. **Split input from output models** - `BookCreate` (what clients send, no `id`) vs `BookPublic` (what you
   return, with `id`). Different contracts deserve different models.
3. The response model **filters output**: any field your internal object has but the output model doesn't
   declare (like `secret_notes`) is stripped before it reaches the client.
4. **Status codes** say what happened: `200` (default success), `201` (created), `204` (no content), `404`
   (not found), `422` (validation failed, set automatically). Set the success code with `status_code=`.
5. **`HTTPException`** is how you signal errors - `raise` it (don't return) with a status and `detail`, and
   FastAPI sends a clean, consistent JSON error.
6. ⚠️ One model for everything leaks data and accepts bad input - separate them from the start and your API
   contract stays safe and accurate as it grows.

## Quick check

Test yourself on the one idea that anchors this phase - input and output are different contracts:

```quiz
[
  {
    "q": "You return an internal object that has a `secret_notes` field, but your endpoint declares `response_model=BookPublic` (which has no `secret_notes`). What does the client receive?",
    "choices": [
      "The response with `secret_notes` stripped out - response_model filters the output to only its declared fields",
      "The full object including `secret_notes`, because you returned it",
      "A 500 error, because the object has an extra field",
      "A 422 error, because the output failed validation"
    ],
    "answer": 0,
    "explain": "response_model is a filter. FastAPI runs your return value through BookPublic, which only declares id/title/author, so secret_notes never reaches the client. This is exactly why you split output models from internal ones."
  },
  {
    "q": "Why use a separate `BookCreate` (no `id`) for input instead of one shared `Book` model that includes `id`?",
    "choices": [
      "So clients can't set the `id` themselves - the server controls server-assigned fields",
      "Because Pydantic can't validate a model that has an `id` field",
      "Because FastAPI requires every endpoint to use a different model",
      "Because it makes the JSON response smaller"
    ],
    "answer": 0,
    "explain": "If the input model has an `id`, a client can choose its own id. Keeping `id` out of BookCreate means the server is fully in control of it. That's the safety half of the input/output split."
  },
  {
    "q": "A client requests a book id that doesn't exist. What's the right way to respond with a 404?",
    "choices": [
      "`raise HTTPException(status_code=404, detail=\"Book not found\")`",
      "`return {\"error\": 404, \"message\": \"Book not found\"}`",
      "`return None` and let FastAPI figure out it's missing",
      "Set `status_code=404` on the decorator so every response is a 404"
    ],
    "answer": 0,
    "explain": "You raise HTTPException, not return an error dict. Raising short-circuits the endpoint and FastAPI converts it into a clean JSON error with the right status. Returning a dict would send a 200 with an error-shaped body, and decorator status_code would wrongly apply to all responses."
  }
]
```


---

# Dependency Injection with Depends()

By now your Book service can take a validated request body, shape a clean response, and return correct
status codes. But look closely and the same chores keep repeating: every list endpoint re-reads `skip`
and `limit`, every protected endpoint re-checks the same token, every database-touching endpoint will (in
two phases) open and close a session the same way. Copy-paste that into ten endpoints and you've signed
up to fix the same bug ten times. FastAPI has a built-in answer, and it's one of the framework's best
ideas - trivial syntax once the *shape* clicks.

## The idea: declare what you need, the framework provides it

📝 **Dependency injection** in FastAPI means: a path operation *declares* what it needs as a parameter,
and FastAPI *provides* it by calling a function you wrote - a **dependency**. You don't fetch the thing
yourself. You announce "I need a current user / a database session / pagination settings," and the
framework runs the right function and hands you the result, already prepared.

If "the framework calls your function for you" feels familiar, it should - it's **inversion of control**,
the exact idea behind the word "framework"
([What a Framework Even Is](/guides/what-a-framework-even-is)). You're not calling the dependency; you
register what you want, and FastAPI calls it on your behalf at request time. *Don't call us, we'll call
you.*

💡 The lovely part: a FastAPI dependency is **just a plain function**. No special base class, no DI
container to configure, no XML, no registration step. If you can write a function, you can write a
dependency. That's the whole system.

## A simple dependency

Pagination - `skip` and `limit` - is the textbook case, because every list endpoint in the Book service
wants the same two query parameters with the same defaults and the same sanity checks.

The dependency itself is pure Python - no FastAPI imports, no running server needed - so it runs right
here:

```python runnable
def pagination_params(skip: int = 0, limit: int = 10) -> dict:
    # one place to keep pagination sane for the whole API
    safe_limit = min(limit, 100)   # never let a caller ask for 10,000 rows
    return {"skip": max(skip, 0), "limit": safe_limit}

# simulate FastAPI calling it with values pulled from the query string
print(pagination_params())                 # the defaults
print(pagination_params(skip=20, limit=5)) # a normal request
print(pagination_params(skip=-3, limit=999))  # a hostile request, clamped
```

*What just happened:* `pagination_params` is an ordinary function with two parameters that have
defaults. It clamps nonsense - negative `skip`, a `limit` of 999 - into something safe and returns a
tidy dict. Nothing here is FastAPI-specific yet; it's testable and runnable in isolation.

Wire it into an endpoint. This part needs the running app, so it's shown as plain code:

```python
from fastapi import FastAPI, Depends

app = FastAPI()

# pretend store; real DB arrives in Phase 7
BOOKS = [{"id": i, "title": f"Book {i}"} for i in range(1, 51)]

def pagination_params(skip: int = 0, limit: int = 10) -> dict:
    return {"skip": max(skip, 0), "limit": min(limit, 100)}

@app.get("/books")
def list_books(params: dict = Depends(pagination_params)):
    start = params["skip"]
    end = start + params["limit"]
    return BOOKS[start:end]
```

*What just happened:* `params: dict = Depends(pagination_params)` is the whole trick. You did **not**
call `pagination_params()` yourself - you handed the function to `Depends()`. When a request hits
`/books`, FastAPI calls it for you and injects the returned dict as `params`. Your endpoint body never
touches `skip` or `limit` directly; it just receives a ready-made, already-clamped dict.

💡 The part people miss the first time: the dependency's *own* parameters become part of the endpoint's
public interface. Because `pagination_params` declares `skip` and `limit`, a request to
`/books?skip=20&limit=5` works - FastAPI reads those query params, validates them as `int` (the same
type-hint-driven validation from earlier phases), passes them in, and they even show up in the automatic
`/docs`. The dependency contributed query parameters to an endpoint that never mentions them.

## Why DI here is genuinely powerful

That pagination example is small, but the payoff scales. Three wins, all from the same mechanism:

- **Write shared logic once.** Add `params: dict = Depends(pagination_params)` to `/books`,
  `/authors`, `/reviews` - every list endpoint gets identical, sane pagination. Change the max limit in
  *one* function and the whole API updates. No copy-paste, no drift.
- **Testable by swapping.** Because the dependency is just a function, your tests can replace it with a
  fake (FastAPI has a `dependency_overrides` hook for exactly this). Need a test to run as an admin user?
  Override the auth dependency to return one. No real tokens, no real database. We'll lean on this in the
  testing phase.
- **Self-documenting.** A dependency's parameters flow into the interactive docs automatically, so the
  contract stays accurate without you maintaining it by hand.

And dependencies compose. 📝 A dependency can itself depend on another via `Depends(...)` - these are
**sub-dependencies**, and FastAPI resolves the whole chain for you, in order, before your endpoint runs:

```python
from fastapi import Depends

def db_connection():
    return {"conn": "fake-connection"}

# this dependency needs the one above - a sub-dependency
def book_repository(db: dict = Depends(db_connection)):
    return {"repo": "books", "using": db["conn"]}

@app.get("/books/{book_id}")
def get_book(book_id: int, repo: dict = Depends(book_repository)):
    return {"book_id": book_id, "served_by": repo}
```

*What just happened:* `get_book` asks only for `book_repository`. But `book_repository` itself asks for
`db_connection`. FastAPI walks the chain: it calls `db_connection` first, feeds the result into
`book_repository`, then hands *that* result to your endpoint. You declared one need at the top and the
framework assembled the whole stack underneath - DB → repository → endpoint, no glue code.

## `yield` dependencies: setup before, teardown after

Some things you depend on need to be *opened* and then reliably *closed* - a database session, a file, a
network client. You want code to run **before** the request handler and more **after** it, even if the
handler blew up. FastAPI's answer is a dependency that uses `yield` instead of `return`.

📝 A **`yield` dependency** runs everything up to the `yield` as **setup**, hands the yielded value to
your endpoint, and runs everything after the `yield` as **teardown** once the response is sent. If you've
met context managers in Python ([Python From Zero](/guides/python-from-zero) covers the `with`
statement), this is the same setup/teardown shape - the code after `yield` is your `finally`.

The canonical use is a database session. The real version lands in Phase 7; here's the *shape* now:

```python
from fastapi import Depends

# stand-in for a real session; Phase 7 makes this a SQLModel Session
def get_db():
    db = {"session": "open", "queries": []}   # setup: open the session
    print("DB session opened")
    try:
        yield db                              # hand it to the endpoint
    finally:
        print("DB session closed")           # teardown: always runs

@app.get("/books-from-db")
def list_books_from_db(db: dict = Depends(get_db)):
    db["queries"].append("SELECT * FROM books")
    return {"books": [], "ran": db["queries"]}
```

*What just happened:* When a request arrives, FastAPI runs `get_db` up to `yield`, opening the "session"
and injecting it as `db`. Your endpoint uses it. After the response is sent, FastAPI resumes the function
past `yield` and runs the teardown - closing the session. One function owns the entire lifecycle of the
resource, so an endpoint can *never* forget to clean up.

⚠️ The teardown runs **even if your endpoint raises an exception** - that's the entire reason for the
`try`/`finally`: a request that errors out still closes its database session, so you don't leak
connections every time something goes wrong. This is the single most important reason DB sessions are
done as `yield` dependencies and not opened ad hoc inside handlers.

## Auth as a dependency (preview) + where you can attach it

The same mechanism is how authentication works in FastAPI - a perfect fit. "This endpoint requires a
logged-in user" is exactly "this endpoint *depends on* there being a current user." Full auth - real
tokens, OAuth2, JWT - is Phase 8; here's the shape so you see the seam:

```python
from fastapi import Depends, Header, HTTPException

def get_current_user(x_token: str = Header(default="")):
    if x_token != "secret-token":
        # no valid credentials → stop here, the endpoint never runs
        raise HTTPException(status_code=401, detail="Not authenticated")
    return {"username": "ada", "role": "reader"}

@app.post("/books")
def create_book(title: str, user: dict = Depends(get_current_user)):
    return {"created_by": user["username"], "title": title}
```

*What just happened:* `create_book` depends on `get_current_user`. FastAPI runs that dependency *first*;
if the token is missing or wrong it raises `401` and your endpoint body never executes. If it passes, the
endpoint receives the authenticated `user`. Same pattern as pagination - declare the need, the framework
satisfies (or rejects) it before you run. (`get_current_user` could itself `Depends` on `get_db` to look
the user up - sub-dependencies again.)

You don't have to attach a dependency endpoint-by-endpoint. There are three reuse levels:

- **Path level** - in the endpoint's parameters, as above. Affects that one route.
- **Router level** - `APIRouter(dependencies=[Depends(get_current_user)])` applies it to every route on
  that router. Great for "everything under `/admin` requires auth."
- **App level** - `FastAPI(dependencies=[Depends(...)])` applies it to *every* endpoint, ideal for
  cross-cutting concerns like a global API-key check.

(You'll meet routers properly in Phase 9; the takeaway now is that the *same* `Depends()` scales from one
route to the whole app.)

💡 `Depends()` is the backbone you'll lean on for the rest of this guide. Database sessions in Phase 7
and authentication in Phase 8 are both *just dependencies*. Learn this one mechanism well and those
phases become "apply the thing you already know" rather than new machinery.

## Recap

1. **Dependency injection** = an endpoint declares what it needs as a parameter (`Depends(func)`), and
   FastAPI calls your function and injects the result. It's **inversion of control** - the framework
   calls your code, not the other way around.
2. A dependency is **a plain function**. No container, no base class. Its own parameters become part of
   the endpoint's interface and show up in the automatic docs.
3. DI lets you **write shared logic once** (pagination, auth, DB), makes endpoints **testable** by
   swapping dependencies, and keeps the API **self-documenting**. Dependencies can depend on other
   dependencies (**sub-dependencies**), resolved as a chain.
4. A **`yield` dependency** runs setup before the request and teardown after - the standard pattern for
   opening and closing resources like DB sessions, mirroring Python's `with`.
5. The teardown of a `yield` dependency runs **even when the endpoint raises**, which is exactly why DB
   sessions use it - errored requests still clean up.
6. **Auth fits naturally as a dependency**, and dependencies attach at the **path, router, or app**
   level. `Depends()` is the backbone for databases and auth in the phases ahead.

## Quick check

Lock in the core idea before moving on:

```quiz
[
  {
    "q": "When you write `params: dict = Depends(pagination_params)`, who calls `pagination_params`?",
    "choices": ["You call it yourself before the endpoint runs", "FastAPI calls it and injects the result", "It is never called; Depends just documents it", "Pydantic calls it during model validation"],
    "answer": 1,
    "explain": "That's inversion of control: you declare the dependency and FastAPI calls the function for you at request time, passing its return value into your endpoint."
  },
  {
    "q": "Why is a database session typically written as a `yield` dependency with try/finally?",
    "choices": ["yield makes the query run faster", "So the session is shared globally across all requests", "So teardown (closing the session) runs after the response, even if the endpoint raised", "Because FastAPI cannot inject objects created with return"],
    "answer": 2,
    "explain": "Code after `yield` runs as teardown once the response is sent, and the `finally` guarantees it runs even on an exception - so errored requests still close their session and don't leak connections."
  },
  {
    "q": "You want every route under an admin router to require authentication. What's the cleanest place to attach `Depends(get_current_user)`?",
    "choices": ["On each endpoint individually, repeated everywhere", "At the router level, e.g. APIRouter(dependencies=[Depends(get_current_user)])", "Inside the Pydantic response model", "It can only ever be attached per-path"],
    "answer": 1,
    "explain": "Dependencies attach at the path, router, or app level. Putting it on the router applies it to every route on that router in one place - no per-endpoint repetition."
  }
]
```


---

# Async & Concurrency

This is the phase where FastAPI either clicks or burns you. People hear "FastAPI is async, async is fast"
and sprinkle `async def` on everything like seasoning - then one slow database call quietly freezes their
entire server under load. There's a tiny mental model underneath all of it, and once you have it, the
rules write themselves. Build the model first, then the rules, then the one trap that catches almost
everyone.

## The mental model: one thread that refuses to wait

📝 **The event loop.** FastAPI runs on an **ASGI** server (Uvicorn), and at its heart is a single thread
running an **event loop**. That one thread serves *many* concurrent requests - not by cloning itself, but
by never sitting idle. When a request hits a point where it has to *wait* (a database round-trip, an HTTP
call to another service), the loop doesn't block on it. It parks that request and goes to run another one.
When the awaited thing is ready, it comes back and picks up where it left off.

If you've met this idea in JavaScript, it's the *same* idea - one loop, cooperative switching at `await`
points. The full machinery is in [Async/Await & the Event Loop](/guides/async-await-and-the-event-loop).
Under the hood it's Python's `asyncio`, the same model (and the same GIL caveat) covered in
[Python From Zero](/guides/python-from-zero).

💡 The key insight: concurrency here doesn't come from *more threads*. It comes from **not blocking**
while waiting. One thread can keep hundreds of requests in flight as long as each one steps aside (via
`await`) during its waits instead of hogging the loop.

The gesture, in pure Python - no server needed, run it and watch the order:

```python runnable
import asyncio

async def fetch(name, seconds):
    print(f"{name} starting")
    await asyncio.sleep(seconds)     # stands in for a network/DB wait; yields the loop here
    print(f"{name} done after {seconds}s")
    return name

async def main():
    # kick off two "requests" concurrently on ONE thread
    results = await asyncio.gather(
        fetch("request-A", 2),
        fetch("request-B", 1),
    )
    print("both finished:", results)

asyncio.run(main())
```

*What just happened:* both `fetch` calls started immediately. At each `await asyncio.sleep(...)`, the
task said "I'm about to wait - go run something else," and the single loop switched to the other one. So
`B` (1s) finished before `A` (2s), and the whole thing took about **2 seconds, not 3**. That's the event
loop: one thread, overlapping the waits. `await` means "I might pause here; let the loop do other work" - 
exactly how your `async def` endpoints share one thread across many requests.

## `async def` vs `def` path operations - FastAPI accepts both

Something that surprises people: FastAPI happily takes endpoints written **either way**, and it does
something different with each.

📝 **The two paths:**

- An **`async def`** endpoint runs **directly on the event loop**. It shares the loop with every other
  request, so it must never block (more on that in a moment).
- A plain **`def`** endpoint is run in a **threadpool** - FastAPI offloads it to a separate worker thread
  so that even if it blocks, it can't freeze the loop.

Both are first-class. FastAPI isn't tolerating `def` as a legacy thing; it's a deliberate, correct choice
for a whole category of work. The question is never "which is faster" - it's "what does my endpoint *do*
inside?"

```python
from fastapi import FastAPI

app = FastAPI()

@app.get("/books/async")
async def list_books_async():
    # runs ON the event loop - fine, because there's nothing blocking here
    return {"books": ["Dune", "Neuromancer"]}

@app.get("/books/sync")
def list_books_sync():
    # runs in a THREADPOOL - FastAPI moved it off the loop for us
    return {"books": ["Dune", "Neuromancer"]}
```

*What just happened:* both endpoints return the same thing and both work perfectly. The only difference
is *where they run*. `list_books_async` executes on the loop's thread; `list_books_sync` gets handed to a
threadpool worker. For trivial bodies like these it doesn't matter - the difference becomes everything
the moment real work (a DB call, an HTTP request) shows up inside.

## The rule: match the keyword to the work

💡 You don't have to guess. There's a single decision:

> **Use `async def` when you `await` truly async I/O. Use plain `def` when your work is blocking or
> synchronous - FastAPI will move it to a thread for you.**

- Calling an **async** library - an async database driver, `httpx.AsyncClient`, an async cache client?
  Write **`async def`** and `await` it. You stay on the loop and yield politely during the wait.
- Calling a **sync/blocking** library - a synchronous DB driver, `requests`, file I/O, or CPU work? Write
  plain **`def`**. FastAPI runs it in the threadpool so its blocking can't stall the loop.

Here are both, done right:

```python
import httpx
from fastapi import FastAPI

app = FastAPI()

# ASYNC work → async def + await
@app.get("/books/{book_id}/cover")
async def get_cover(book_id: int):
    async with httpx.AsyncClient() as client:
        resp = await client.get(f"https://covers.example.com/{book_id}")  # awaits - yields the loop
    return {"book_id": book_id, "cover_url": resp.json()["url"]}

# BLOCKING work → plain def (threadpool)
@app.get("/books/report")
def generate_report():
    import time
    time.sleep(2)                 # a blocking, synchronous operation (stand-in for a sync DB / heavy lib)
    return {"report": "ready"}
```

*What just happened:* `get_cover` does real network I/O with an **async** client, so it's `async def` and
`await`s the call - while it waits for the cover service, the loop serves other requests.
`generate_report` calls something **blocking** (`time.sleep`, standing in for a sync DB query or a
blocking library), so it's plain `def` - FastAPI runs it in a threadpool worker, and the event loop stays
free the whole time. Each keyword matches what's actually inside the body - the entire rule.

## ⚠️ The cardinal sin: blocking the event loop

The single most common FastAPI performance bug, and it looks completely innocent.

⚠️ **Never call a blocking function inside an `async def`.** If you put a synchronous DB call, a
`requests.get()`, or a `time.sleep()` directly inside an `async def` endpoint, you don't just slow down
*that* request - you freeze the **entire event loop**. One thread serves everyone. While that thread
sits inside a blocking call, it cannot switch to any other request. Every concurrent user stalls until
your one slow call returns.

The bug runs, returns the right answer, and quietly destroys your throughput under load:

```python
import time
from fastapi import FastAPI

app = FastAPI()

@app.get("/books/slow")
async def slow_books():
    time.sleep(2)        # 🚨 BLOCKING call inside async def - freezes the whole loop for 2 seconds
    return {"books": ["Dune"]}
```

*What just happened:* `time.sleep(2)` is *synchronous*. It blocks the thread it runs on - and that
thread is the event loop. For those 2 seconds, **no other request can be served**, no matter how fast
those other requests are. One user hitting this endpoint makes everyone else wait. It works fine when
tested alone, which is exactly why this bug ships to production and only shows up when traffic arrives.

There are three straightforward fixes. Pick by what the blocking thing actually is:

**Fix 1 - make it truly async.** If an async equivalent exists, use it and `await`:

```python
import asyncio
from fastapi import FastAPI

app = FastAPI()

@app.get("/books/slow")
async def slow_books():
    await asyncio.sleep(2)     # ✅ async wait - yields the loop; other requests run during these 2s
    return {"books": ["Dune"]}
```

*What just happened:* `await asyncio.sleep(2)` waits *cooperatively*. Instead of holding the thread
hostage, it hands the loop back so other requests run during the wait. Same 2-second delay for *this*
caller, zero impact on everyone else. (In real code: swap the sync DB driver for an async one, swap
`requests` for `httpx.AsyncClient`.)

**Fix 2 - use plain `def`.** If there's no async version of the library, drop `async` and let FastAPI's
threadpool handle the blocking:

```python
import time
from fastapi import FastAPI

app = FastAPI()

@app.get("/books/slow")
def slow_books():               # ✅ plain def → runs in the threadpool, off the loop
    time.sleep(2)               # blocking is fine here; it's not on the event loop's thread
    return {"books": ["Dune"]}
```

*What just happened:* by removing `async`, the endpoint runs in a threadpool worker. Now `time.sleep`
blocks *that worker thread*, not the event loop - so the loop keeps serving everyone else. Often the
simplest fix when you're stuck with a synchronous library.

**Fix 3 - offload from inside an `async def`.** Sometimes you're already in an `async def` (maybe you
`await` something else too) but you *have* to call one blocking function. Use `run_in_threadpool`:

```python
import time
from fastapi import FastAPI
from fastapi.concurrency import run_in_threadpool

app = FastAPI()

@app.get("/books/slow")
async def slow_books():
    await run_in_threadpool(time.sleep, 2)   # ✅ push the blocking call onto a worker thread, await it
    return {"books": ["Dune"]}
```

*What just happened:* `run_in_threadpool` shoves the blocking `time.sleep` onto a worker thread and gives
you back an awaitable. You `await` it, so the loop is free during the wait, and the blocking call happens
safely off-loop. The escape hatch for "I'm in async-land but this one library is stubbornly sync."

🪖 **War story.** A team ships an `async def` endpoint that calls their old synchronous Postgres driver
directly. Tests pass, demo is snappy. In production, the moment more than a handful of users hit it at
once, *every* endpoint on the service crawls - health checks time out, the load balancer starts killing
pods. The fix was one keyword: delete `async`. The blocking driver moved to the threadpool and the loop
was free again. Knowing this rule turns a 3am incident into a non-event.

## CPU-bound work, and a real limit

⚠️ The part the hype skips: **async helps with I/O-bound concurrency, not CPU-bound work.** Async is
about overlapping *waiting*. If your endpoint does heavy computation - resizing images, crunching a giant
dataset, hashing in a loop - there's no waiting to overlap. Because of Python's **GIL** (the full story is
in [Python From Zero](/guides/python-from-zero)), threads don't give true parallelism for pure-Python CPU
work either. So even a plain `def` in the threadpool won't make CPU work *parallel* - it just keeps it
off the event loop.

For genuinely heavy CPU work, neither `async def` nor the threadpool is the answer. Reach for a **process
pool** (separate processes, separate GILs, real parallelism) or push the job to a **background worker
queue** (Celery, RQ, Dramatiq) and have the endpoint return quickly with a job id.

💡 **The whole takeaway:** match the keyword to the work. `async def` + `await` for async I/O. Plain
`def` for blocking or synchronous I/O. Never block the loop. For heavy CPU, get off the web process
entirely. Get this right and FastAPI's speed is yours; get it wrong and one sleepy call takes the whole
server down.

## Recap

1. **FastAPI runs on a single-threaded event loop** (ASGI/Uvicorn). It serves many concurrent requests by
   *not blocking* during waits - at each `await`, it parks one request and runs another.
2. FastAPI accepts **both** endpoint styles: `async def` runs **on the loop**; plain `def` runs in a
   **threadpool** so its blocking can't stall the loop. Neither is "the fast one" - they're for different work.
3. **The rule:** `async def` + `await` when you call truly async I/O (async DB driver, `httpx`); plain `def`
   when the work is blocking/synchronous (`requests`, sync driver, file I/O) - FastAPI offloads it.
4. **The cardinal sin:** a blocking call (`time.sleep`, sync DB, `requests.get`) inside an `async def`
   freezes the *entire* loop, stalling every concurrent request. The #1 FastAPI performance bug.
5. **Three fixes:** make it truly async (`await` an async equivalent), switch to plain `def` (threadpool),
   or `run_in_threadpool(...)` from inside an `async def`.
6. **Async is for I/O-bound concurrency, not CPU-bound work** - the GIL means threads don't parallelize
   pure-Python computation. Use a process pool or a background worker for heavy CPU.

## Quick check

Lock in the model that keeps your server alive under load:

```quiz
[
  {
    "q": "Why does calling time.sleep(2) (a blocking call) inside an async def endpoint hurt every request, not just that one?",
    "choices": ["It uses too much memory", "FastAPI serves async def endpoints on a single event-loop thread; a blocking call holds that thread, so no other request can be served until it returns", "time.sleep is deprecated in async code", "It opens a new database connection for every caller"],
    "answer": 1,
    "explain": "async def runs on the one event-loop thread. A synchronous/blocking call holds that thread hostage, so the loop can't switch to any other request - everyone stalls for the full duration."
  },
  {
    "q": "Your endpoint must call a synchronous, blocking database driver (no async version available). What's the cleanest correct choice?",
    "choices": ["Write it as async def and call the driver directly", "Write it as a plain def so FastAPI runs it in the threadpool", "Wrap the whole thing in asyncio.run()", "Add more Uvicorn workers and call it from async def anyway"],
    "answer": 1,
    "explain": "A plain def endpoint is run in FastAPI's threadpool, so the blocking driver blocks a worker thread, not the event loop. (Or, from inside an async def, use run_in_threadpool.) Calling a blocking driver directly inside async def freezes the loop."
  },
  {
    "q": "You have a CPU-heavy endpoint (resizing large images in pure Python) and want it to actually use multiple cores. Does making it async def help?",
    "choices": ["Yes - async def automatically parallelizes CPU work", "Yes - the event loop spreads CPU work across cores", "No - async helps overlap I/O waits, not CPU work; and the GIL blocks true thread parallelism. Use a process pool or background worker", "No - CPU work is impossible to speed up in Python at all"],
    "answer": 2,
    "explain": "Async overlaps waiting, and there's no waiting in pure computation. The GIL also prevents threads from parallelizing pure-Python CPU work, so the threadpool won't make it parallel either. Offload to a process pool or a background worker queue."
  }
]
```


---

# Databases with SQLModel

Every Book your service has handled so far has lived in a Python list that vanishes the moment the
process restarts. That was fine while we learned routing, validation, response models, and dependency
injection - but a real service has to *remember* things. [Phase 5](05-dependency-injection.md) built a
`get_db` dependency that opened a fake session, handed it to the endpoint, and closed it afterward. This
phase makes that real: a genuine database, a genuine session, and the four operations every app
eventually needs - create, read, update, delete.

## The mental model: one class, two jobs

📝 A database is a separate program that stores your data as **rows in tables** and guards it - types,
uniqueness, many callers at once. If that's fuzzy, [What a Database Actually Is](/guides/what-a-database-is)
is the gentle version. Your Python code doesn't speak to it in objects; the database speaks **SQL** and
stores **rows**. Something has to translate between "a `Book` object in memory" and "a row in the `book`
table." That translator is an **ORM** (Object-Relational Mapper): it maps objects ↔ rows so you write
Python and it writes the SQL.

If you've met an ORM before - say Java's JPA - the *concepts* transfer almost one-for-one: entities, a
session/persistence context, lazy loading, the N+1 trap. [Hibernate & JPA From
Zero](/guides/hibernate-and-jpa-from-zero) covers those ideas in depth. SQLModel is the same playbook,
Python-flavored.

📝 **SQLModel** - written by Sebastián Ramírez, the same person who wrote FastAPI - sits on top of two
libraries you'd otherwise wire together by hand:

- **Pydantic** gives you validation and serialization (the model layer from [Phase 3](03-pydantic-models-and-validation.md) and [Phase 4](04-response-models-and-status-codes.md)).
- **SQLAlchemy** gives you the ORM and the actual database talking.

SQLModel fuses them so that **one class can be both your API model *and* your database table.** No
duplicating fields across a Pydantic schema and a separate ORM model. The class you validate requests
with can be the same class that maps to a table. That's the whole pitch.

## Defining a table model

Here's `Book` as a real table. This needs a database engine and can't run in the browser sandbox, so
it's plain Python - copy it into a file and run it locally:

```python
from sqlmodel import SQLModel, Field, create_engine

class Book(SQLModel, table=True):
    id: int | None = Field(default=None, primary_key=True)
    title: str
    author: str
    year: int
    price: float

# the engine is the connection factory to the database
engine = create_engine("sqlite:///books.db", echo=True)

# create the table(s) for every table-model SQLModel knows about
SQLModel.metadata.create_all(engine)
```

*What just happened:* `class Book(SQLModel, table=True)` declares a model that is *also* a table. Each
annotated attribute becomes a column with a real SQL type (`title` → `TEXT`, `year` → `INTEGER`, `price`
→ `REAL`). `id: int | None = Field(default=None, primary_key=True)` marks `id` as the **primary key** - 
the unique handle for each row - and `None` by default because the database fills it in on insert.
`create_engine(...)` builds the object that knows how to reach your database (here a local SQLite file;
swap the URL for Postgres in production and nothing else changes). `metadata.create_all` issues the
`CREATE TABLE` statements; `echo=True` prints the SQL it runs.

⚠️ The `table=True` is load-bearing. **With** it, the class maps to a real table. **Without** it,
SQLModel treats the class as a plain Pydantic model - a request/response schema, no table behind it.
You'll use both flavors in this guide: `table=True` means "this is a table"; no `table=True` means "this
is just a shape."

## The session as a dependency

📝 You never talk to the engine directly for ordinary work. You open a **Session** - a short-lived
workspace bound to one unit of work. You add objects to it, query through it, and `commit()` to flush
your changes to the database. Then you close it. That open → use → close lifecycle is *exactly* the
setup/teardown shape the `yield` dependency from Phase 5 was built for.

Make `get_db` real. It was a placeholder printing "session opened" - now it opens an actual `Session`:

```python
from sqlmodel import Session
from fastapi import Depends

def get_session():
    with Session(engine) as session:   # setup: open a session bound to the engine
        yield session                  # hand it to the endpoint
    # teardown: the `with` block closes the session when the request finishes
```

*What just happened:* this is the same yield-dependency you already understand, with the fake dict
swapped for a real `Session`. `with Session(engine) as session` opens a session; `yield session` injects
it into whatever endpoint asked for it; and when the request finishes, the `with` block runs the
session's teardown - closing it, releasing the connection - **even if the endpoint raised.** That's the
whole reason sessions are done this way: an errored request still cleans up and you never leak
connections.

💡 This is *the* clean per-request DB-session pattern, and it's why we spent a phase on `yield`
dependencies before touching a database. One session is born when a request arrives and dies when the
response is sent. Endpoints just declare `session: Session = Depends(get_session)` and receive a
ready-to-use session - they never open or close one themselves.

## CRUD: the four operations

Now the payoff. The Book endpoints, each receiving an injected session and doing real database work:

```python
from fastapi import FastAPI, Depends, HTTPException
from sqlmodel import Session, select

app = FastAPI()

@app.post("/books")
def create_book(book: Book, session: Session = Depends(get_session)):
    session.add(book)       # stage the new row
    session.commit()        # write it to the database
    session.refresh(book)   # reload it so book.id is populated
    return book

@app.get("/books")
def list_books(session: Session = Depends(get_session)):
    books = session.exec(select(Book)).all()   # SELECT * FROM book
    return books

@app.get("/books/{book_id}")
def get_book(book_id: int, session: Session = Depends(get_session)):
    book = session.get(Book, book_id)   # fetch by primary key
    if book is None:
        raise HTTPException(status_code=404, detail="Book not found")
    return book
```

*What just happened:* three operations, all through the injected session.
- **Create** - `session.add(book)` stages the object, `session.commit()` writes it, and
  `session.refresh(book)` reloads it from the database so the auto-generated `id` is filled in before you
  return it. (Skip the refresh and `book.id` is still `None` in your response.)
- **Read all** - `select(Book)` builds a query, `session.exec(...).all()` runs it and returns a list of
  `Book` objects.
- **Read one or 404** - `session.get(Book, book_id)` looks a row up by primary key; if it's missing, raise
  the correct `404` from [Phase 4](04-response-models-and-status-codes.md) instead of returning `null`.

Update and delete round out the set:

```python
@app.put("/books/{book_id}")
def update_book(book_id: int, new_price: float, session: Session = Depends(get_session)):
    book = session.get(Book, book_id)
    if book is None:
        raise HTTPException(status_code=404, detail="Book not found")
    book.price = new_price   # mutate the tracked object
    session.add(book)
    session.commit()
    session.refresh(book)
    return book

@app.delete("/books/{book_id}")
def delete_book(book_id: int, session: Session = Depends(get_session)):
    book = session.get(Book, book_id)
    if book is None:
        raise HTTPException(status_code=404, detail="Book not found")
    session.delete(book)
    session.commit()
    return {"deleted": book_id}
```

*What just happened:* update *fetches* the existing row first (so you only change real records), mutates
the attribute, and commits - the session tracks the object, so changing `book.price` and committing is
enough to issue the `UPDATE`. Delete fetches, calls `session.delete(book)`, and commits. Both 404
cleanly when the id doesn't exist. The rhythm across all four: fetch or build → change → `commit`. CRUD.

When `list_books` runs, `select(Book)` becomes a real query. With `echo=True` you'd see SQLModel emit
something like:

```sql
SELECT book.id, book.title, book.author, book.year, book.price
FROM book;
```

And `session.get(Book, 7)` becomes a primary-key lookup:

```sql
SELECT book.id, book.title, book.author, book.year, book.price
FROM book
WHERE book.id = 7;
```

```console
INFO     sqlalchemy.engine.Engine BEGIN (implicit)
INFO     sqlalchemy.engine.Engine SELECT book.id, book.title, book.author, book.year, book.price FROM book
INFO     sqlalchemy.engine.Engine [generated in 0.00018s] ()
```

*What just happened:* you wrote Python (`select(Book)`), the ORM wrote SQL - the whole job of an ORM,
made visible. To read those queries fluently - joins especially - [SQL Joins
Explained](/guides/sql-joins-explained) is the companion.

## Input/output models - and the gotchas

You *can* accept a `Book` table model straight off the request, like `create_book` does above, and it
works. But 💡 keep the same discipline from [Phase 4](04-response-models-and-status-codes.md): use
**separate input and output schemas** even with SQLModel. Make them plain models (no `table=True`):

```python
class BookCreate(SQLModel):           # input: what a client may send
    title: str
    author: str
    year: int
    price: float

class BookPublic(SQLModel):           # output: what you promise to return
    id: int
    title: str
    author: str
    year: int
    price: float

@app.post("/books", response_model=BookPublic)
def create_book(data: BookCreate, session: Session = Depends(get_session)):
    book = Book.model_validate(data)   # build the table object from validated input
    session.add(book)
    session.commit()
    session.refresh(book)
    return book                        # filtered through BookPublic on the way out
```

*What just happened:* `BookCreate` has no `id`, so a client physically *cannot* set the primary key - the
database owns that. `Book.model_validate(data)` turns the validated input into a real table object.
`response_model=BookPublic` filters the response so you control exactly what goes out the door, even
though you returned the full table object. Three layers, one source of truth, no field duplication pain
because they're all SQLModel.

A few traps worth naming before you ship:

⚠️ **Don't return the raw table object if it has fields you don't want exposed.** The moment your table
grows a column like `internal_notes` or, later, a `hashed_password`, returning the bare `Book` leaks it.
`response_model=BookPublic` above is your guarantee that only the public shape escapes - that discipline
matters a lot more in [Phase 8: Authentication & Security](08-authentication-and-security.md).

⚠️ **One session per request - never share one across requests.** A `Session` is a short-lived unit of
work, not a global you create once at startup. Sharing a session between concurrent requests corrupts
state and produces baffling bugs. The `get_session` dependency exists precisely so each request gets its
own fresh session and gives it back. Resist the urge to make `session` a module-level singleton.

⚠️ **The N+1 query trap is still here.** Loop over 100 books and touch a related object (say each book's
reviews) lazily, and the ORM can quietly fire 1 query for the list plus 100 more - one per book. This is
the *same* trap every ORM has, and the fix is the same: load what you need up front (eager loading / a
join) instead of one row at a time. The deep treatment is in [Hibernate & JPA From
Zero](/guides/hibernate-and-jpa-from-zero) - the lesson is portable, only the syntax differs.

💡 A database in FastAPI is the same DB discipline as any serious ORM - sessions as units of work,
separate input/output shapes, watch your queries - wearing Python's clothes. You already knew the
dependency mechanism from Phase 5; this phase just plugged a real session into it.

## Recap

1. An **ORM** maps Python objects ↔ database rows so you write Python and it writes SQL. **SQLModel**
   (by FastAPI's author) fuses **Pydantic** (validation) and **SQLAlchemy** (ORM) so one class can be
   both your API model and your table.
2. `class Book(SQLModel, table=True)` defines a table; each attribute is a column and
   `Field(primary_key=True)` marks the key. `create_engine(...)` connects, `metadata.create_all`
   builds the tables. **Without `table=True`** the class is just a Pydantic schema, no table.
3. The **session** is a short-lived unit of work. Make `get_session` a `yield` dependency that opens a
   `Session`, yields it, and closes it after the request - even on error. Endpoints inject it with
   `session: Session = Depends(get_session)`.
4. **CRUD**: create = `add` + `commit` + `refresh`; read = `session.get(Book, id)` or
   `session.exec(select(Book)).all()`; update = fetch, mutate, `commit`; delete = fetch, `delete`,
   `commit`. Missing rows raise a correct `404`.
5. Keep **separate `BookCreate`/`BookPublic`** schemas so clients can't set `id` and `response_model`
   controls output. Never return a raw table object with secret fields.
6. **One session per request** (never share across requests), and the **N+1 trap** still applies - 
   load related data up front. Same ORM discipline as everywhere, Python-flavored.

## Quick check

Lock in the database fundamentals before we add auth:

```quiz
[
  {
    "q": "What does adding `table=True` to `class Book(SQLModel)` do?",
    "choices": ["Makes the class faster to validate", "Makes the class map to a real database table (instead of being a plain Pydantic schema)", "Automatically creates the database file", "Marks every field as a primary key"],
    "answer": 1,
    "explain": "With table=True the class maps to a real table; without it, SQLModel treats it as an ordinary Pydantic model used as a request/response schema."
  },
  {
    "q": "After `session.add(book)` and `session.commit()`, why call `session.refresh(book)` before returning it?",
    "choices": ["To open a new session", "To validate the input again", "To reload the object so database-generated fields like the auto-incremented id are populated", "To roll back the transaction"],
    "answer": 2,
    "explain": "The database fills in id on insert. Without refresh, book.id is still None in memory, so your response would omit the real id."
  },
  {
    "q": "Why keep a separate `BookCreate` model (no id) for the request body even though SQLModel lets you accept the table model directly?",
    "choices": ["It runs faster", "So clients can't set the primary key and you control what's accepted vs returned", "Because table models can't be used in POST bodies", "To avoid importing Pydantic"],
    "answer": 1,
    "explain": "A BookCreate without an id means a client physically can't set the database-owned primary key, and paired with response_model you control exactly what goes in and what comes out."
  }
]
```


---

# Authentication & Security

Your Book API can now validate requests, shape responses, and talk to a real database. One thing's
missing that every real service needs: a lock on the door. Right now anyone who can reach `/books` can
create, edit, or delete a book. This phase puts a guard at the entrance - and you already know the
guard's job description. In FastAPI, security is built almost entirely on the dependency system from
[Phase 5](05-dependency-injection.md). "This endpoint requires a logged-in user" is just "this endpoint
*depends on* there being a current user."

Auth has a reputation for being scary, mostly from treating it as one giant blob. We'll take it apart
into four small, separate jobs, each approachable on its own.

## Two different jobs: authentication vs authorization

📝 These two words look almost identical and people mix them up constantly, so pin them down now:

- **Authentication** (authn) = *who you are.* Proving your identity. This is logging in: you hand over a
  username and password, and the server confirms you are who you claim to be.
- **Authorization** (authz) = *what you're allowed to do.* Permissions. Once the server knows you're
  Ada, can Ada *delete* this book? Maybe only admins can.

A passport proves who you are (authn). A concert ticket says what you're allowed into (authz). You need
both, and they're different checks. The full mental model - sessions, tokens, OAuth - lives in a
dedicated guide: [Auth vs Authz](/guides/auth-vs-authz). Here we focus on wiring it into FastAPI.

💡 The throughline for this whole phase: **both jobs flow through `Depends()`.** Authentication is a
dependency that figures out *who* is calling. Authorization is a check *inside* that dependency (or a
second one) that decides *whether they may proceed*. Same machinery, two questions.

## Step 1: never store passwords in plaintext

Before anyone can log in, you need somewhere to keep their credentials - and the single most important
rule in this guide is this:

⚠️ **Never store a password as plaintext.** Not in your database, not in a log file, not anywhere. If
your database leaks (and databases leak), every plaintext password is instantly stolen - and because
people reuse passwords, you've also handed attackers their email and bank logins.

📝 Instead you store a **hash**: a one-way scramble of the password. You run the password through a
slow, deliberate hashing algorithm (bcrypt or argon2 are the standard choices) and store the result.
When someone logs in, you hash *what they typed* and compare it to the stored hash - you never need the
original password back, the whole point of "one-way." The deeper story of salting, slow hashing, and why
these algorithms exist is in [How Passwords Are Stored](/guides/how-passwords-are-stored).

In Python, the `passlib` library wraps all of this (shown as plain code - `passlib` isn't guaranteed in
the browser sandbox, so read it, don't run it):

```python
from passlib.context import CryptContext

# bcrypt is a solid, battle-tested default
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")

def hash_password(plain: str) -> str:
    return pwd_context.hash(plain)

def verify_password(plain: str, hashed: str) -> bool:
    return pwd_context.verify(plain, hashed)

# at signup time:
stored = hash_password("correct horse battery staple")
print(stored)   # -> '$2b$12$....' an opaque hash, NOT the password

# at login time:
verify_password("correct horse battery staple", stored)  # True
verify_password("wrong guess", stored)                   # False
```

*What just happened:* `hash_password` turns a plaintext password into an opaque string you can safely
store on a `User` row. `verify_password` re-hashes the login attempt and checks it against that stored
hash, returning `True`/`False` - without ever un-scrambling anything. You store the output of
`hash_password`, and only ever compare with `verify_password`. The plaintext password lives for a
fraction of a second in memory and is never written down.

💡 You didn't write the bcrypt algorithm yourself - deliberate; see the last section.

## Step 2: the OAuth2 password flow + JWT

A user can sign up and we store their hash. Now: how do they *log in*, and how does the server remember
them on the *next* request? HTTP is stateless - each request arrives with no memory of the last one. We
can't make the user re-send their password on every call (that would mean storing it client-side, the
exact thing we're avoiding).

📝 The standard answer for APIs is the **OAuth2 password flow**:

1. The client POSTs username + password *once* to a `/token` endpoint.
2. The server verifies the password against the stored hash.
3. If it checks out, the server hands back a signed **access token**.
4. On every later request, the client sends that token in the header:
   `Authorization: Bearer <token>` - no password needed again.

The token of choice here is a **JWT** (JSON Web Token). 📝 A JWT is a compact, **signed**,
self-contained string carrying a few **claims** (small facts, like "subject: ada" and "expires: 3pm").
"Signed" means the server stamps it with a secret key so it can later verify the token wasn't tampered
with - change one character and the signature breaks.

⚠️ The trap everyone falls into: **a JWT is signed, not *encrypted*.** Anyone holding it can decode and
read the payload (it's just base64 - paste one into jwt.io and you'll see). The signature stops forgery;
it does **not** hide what's inside. Never put secrets (passwords, credit card numbers, anything
sensitive) in a JWT payload.

Here's the `/token` endpoint. It uses `OAuth2PasswordRequestForm`, FastAPI's helper that reads the
standard `username`/`password` form fields the OAuth2 spec expects:

```python
from datetime import datetime, timedelta, timezone
from fastapi import FastAPI, Depends, HTTPException, status
from fastapi.security import OAuth2PasswordRequestForm
from jose import jwt   # python-jose, the usual JWT library

app = FastAPI()

SECRET_KEY = "load-this-from-an-env-var-not-source-code"
ALGORITHM = "HS256"

# pretend user store; in Phase 7 this is a real DB lookup
FAKE_USER = {"username": "ada", "hashed_password": "$2b$12$...", "role": "reader"}

@app.post("/token")
def login(form: OAuth2PasswordRequestForm = Depends()):
    user = FAKE_USER if form.username == FAKE_USER["username"] else None
    if not user or not verify_password(form.password, user["hashed_password"]):
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED,
            detail="Incorrect username or password",
        )
    expire = datetime.now(timezone.utc) + timedelta(minutes=30)
    token = jwt.encode(
        {"sub": user["username"], "role": user["role"], "exp": expire},
        SECRET_KEY,
        algorithm=ALGORITHM,
    )
    return {"access_token": token, "token_type": "bearer"}
```

*What just happened:* `OAuth2PasswordRequestForm = Depends()` is itself a dependency - it pulls the
`username` and `password` out of the form body for you. You verify the password against the stored hash
with the function from Step 1. On success you build a JWT carrying the username (`sub`, "subject"), the
role, and an expiry (`exp`), sign it with your secret, and return it in the shape OAuth2 clients expect:
`{"access_token": ..., "token_type": "bearer"}`. On failure you return a clean `401` - we don't say
*which* field was wrong, so we don't help attackers guess usernames.

A login request and its response look like this:

```http
POST /token HTTP/1.1
Content-Type: application/x-www-form-urlencoded

username=ada&password=correct+horse+battery+staple
```

```json
{
  "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZGEi...",
  "token_type": "bearer"
}
```

*What just happened:* the client sent form-encoded credentials (not JSON - the OAuth2 password flow uses
a form body) and got back a token. The client stashes that `access_token` and attaches it to every future
request, never sending the password again until the token expires.

## Step 3: securing endpoints with a dependency

Now the payoff: turn "is this caller logged in?" into a dependency, and any endpoint that wants
protection `Depends` on it.

First, declare the scheme. `OAuth2PasswordBearer` tells FastAPI *where* tokens come from (the
`Authorization: Bearer` header) and which URL issues them (so `/docs` can offer a login button):

```python
from fastapi import Depends, HTTPException, status
from fastapi.security import OAuth2PasswordBearer
from jose import jwt, JWTError

oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")

def get_current_user(token: str = Depends(oauth2_scheme)) -> dict:
    creds_error = HTTPException(
        status_code=status.HTTP_401_UNAUTHORIZED,
        detail="Could not validate credentials",
        headers={"WWW-Authenticate": "Bearer"},
    )
    try:
        payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
    except JWTError:
        raise creds_error          # bad signature, expired, or malformed
    username = payload.get("sub")
    if username is None:
        raise creds_error
    # in Phase 7, load the real row: user = db.get(User, username)
    return {"username": username, "role": payload.get("role")}
```

*What just happened:* `Depends(oauth2_scheme)` extracts the raw token string from the `Authorization`
header (and auto-rejects requests that have none). `jwt.decode` verifies the signature **and** the
expiry against your secret - if the token was forged, tampered with, or expired, it raises `JWTError`
and we turn that into a `401`. If it's valid, we read the `sub` claim and return the user. An ordinary
dependency, exactly like the ones in Phase 5 - it just happens to do auth.

Protect the write endpoint. Any route that requires a login adds one parameter:

```python
@app.post("/books", status_code=status.HTTP_201_CREATED)
def create_book(title: str, current_user: dict = Depends(get_current_user)):
    return {"created_by": current_user["username"], "title": title}
```

*What just happened:* `current_user: dict = Depends(get_current_user)` is the entire lock. FastAPI runs
`get_current_user` *before* your function body. No valid token? It raises `401` and `create_book` never
executes. Valid token? Your endpoint receives the authenticated user and can record who created the book.
Zero auth logic inside the handler - it just declares the need.

A request with no token gets stopped cold:

```http
POST /books?title=Dune HTTP/1.1
```

```json
{ "detail": "Not authenticated" }
```

*What just happened:* the request arrived with no `Authorization` header, so `oauth2_scheme` rejected it
with `401` before `get_current_user` even ran. Send the header - `Authorization: Bearer eyJhbGci...` - 
and it sails through. 💡 As a bonus, because you declared `OAuth2PasswordBearer`, FastAPI adds an
**Authorize** button to `/docs`: testers paste a token once and the interactive docs send it on every call.

### Authorization: checking what they're allowed to do

That covered authn (who you are). For authz (what you may do), do the check inside a dependency too - 
one that requires the admin role:

```python
def require_admin(current_user: dict = Depends(get_current_user)) -> dict:
    if current_user["role"] != "admin":
        raise HTTPException(
            status_code=status.HTTP_403_FORBIDDEN,
            detail="Admins only",
        )
    return current_user

@app.delete("/books/{book_id}")
def delete_book(book_id: int, admin: dict = Depends(require_admin)):
    return {"deleted": book_id, "by": admin["username"]}
```

*What just happened:* `require_admin` is a **sub-dependency** - it depends on `get_current_user` to
establish *who* the caller is, then checks their `role`. A logged-in non-admin gets `403 Forbidden`
(`401` means "I don't know who you are," `403` means "I know who you are and you can't do this"). Same
dependency machinery, now answering the authorization question.

## Step 4: the security must-knows

Auth code is the worst place to improvise. A handful of rules carry most of the safety:

- ⚠️ **Always HTTPS in production.** A Bearer token in a plaintext HTTP request is sitting in the open - 
  anyone on the network path can copy it and impersonate the user. TLS is what stops that; see
  [HTTPS & TLS](/guides/https-and-tls). Locally `http://localhost` is fine; in production it is not.
- ⚠️ **Keep the JWT secret out of your code.** That `SECRET_KEY` signs every token. If it leaks,
  attackers can forge valid tokens for any user. Load it from an environment variable (or a secrets
  manager), never commit it, and rotate it if it's ever exposed.
- ⚠️ **Set token expiry, and consider refresh tokens.** Short-lived access tokens (minutes to an hour)
  limit the damage if one is stolen. The common pattern is a short access token plus a longer-lived
  *refresh* token used only to mint new access tokens.
- ⚠️ **Don't put sensitive data in the JWT payload.** It's decodable, remember - username and role are
  fine; passwords, card numbers, and PII are not.
- 💡 **Don't roll your own crypto.** Use FastAPI's OAuth2 utilities and battle-tested libraries
  (`passlib`, `python-jose`). Hand-written hashing or token signing is how subtle, catastrophic bugs get
  in. Security is the one area where "clever" is a red flag - boring and standard wins.

Notice how little new machinery this phase actually introduced. Hashing is two function calls. The token
endpoint is a normal POST handler. Protecting routes is the *same* `Depends()` you already knew - 
`get_current_user` is just a dependency that reads a header and validates a token. Auth felt big because
it bundles four jobs together; taken one at a time, each is something you can already do.

## Recap

1. **Authentication** (who you are) and **authorization** (what you may do) are two distinct checks - 
   and in FastAPI both flow through the dependency system from Phase 5.
2. **Never store plaintext passwords.** Hash them with bcrypt/argon2 via `passlib`: store the hash,
   verify login attempts against it, and never recover the original.
3. The **OAuth2 password flow** takes username/password at a `/token` endpoint (`OAuth2PasswordRequestForm`),
   verifies the hash, and returns a signed **JWT** the client resends as `Authorization: Bearer <token>`.
4. A JWT is **signed, not encrypted** - its payload is readable by anyone, so it stops forgery but hides
   nothing. Keep secrets out of it.
5. Protect endpoints with `OAuth2PasswordBearer` + a `get_current_user` dependency that decodes and
   validates the token; add `Depends(get_current_user)` to require auth (and a role check inside a
   sub-dependency for authorization). FastAPI even adds the Authorize button to `/docs`.
6. **Security must-knows:** always HTTPS in prod, keep the JWT secret in env vars, set token expiry, no
   sensitive data in the payload, and lean on standard libraries instead of rolling your own crypto.

## Quick check

Make sure the core ideas stuck before moving on:

```quiz
[
  {
    "q": "Your database leaks. Which storage choice means the attacker does NOT instantly have everyone's usable passwords?",
    "choices": ["Passwords stored as plaintext", "Passwords stored as bcrypt/argon2 hashes", "Passwords stored base64-encoded", "Passwords stored inside the JWT payload"],
    "answer": 1,
    "explain": "A one-way hash (bcrypt/argon2) can't be reversed to the original password. Plaintext and base64 are both readable; base64 is just encoding, not protection."
  },
  {
    "q": "True or false: because a JWT is signed, the data inside it is hidden from anyone holding the token.",
    "choices": ["True - signing encrypts the payload", "False - a JWT is signed, not encrypted; the payload is readable", "True, but only if you use HS256", "False - JWTs have no payload at all"],
    "answer": 1,
    "explain": "Signing proves the token wasn't tampered with; it does not hide the contents. Anyone can base64-decode and read a JWT payload, so never put secrets in it."
  },
  {
    "q": "In a protected endpoint, what does `current_user: dict = Depends(get_current_user)` accomplish?",
    "choices": ["It encrypts the response", "FastAPI runs get_current_user first; an invalid/missing token raises 401 and the handler never runs", "It logs the user out after the request", "It stores the password in the session"],
    "answer": 1,
    "explain": "Auth is just a dependency. FastAPI resolves get_current_user before the body; if the token is missing or invalid it raises 401 and the endpoint never executes."
  }
]
```


---

# Testing & Project Structure

The moment your Book API touches a real database and real auth, the temptation is to test it by spinning
up the server, opening `/docs`, and clicking around. That works exactly once, on your machine, on a good
day. It doesn't catch the bug you introduce next Tuesday, and it can't run in CI.

This phase is about two habits that travel together: testing FastAPI *properly* - fast, repeatable,
in-process, no clicking - and laying out the project so it stays testable as it grows. These aren't
separate topics: the reason FastAPI is so pleasant to test is the exact design we've been building
toward - everything is a dependency - and that same design keeps the codebase from collapsing into one
unreadable file.

## The mental model: your app is a callable, not a server

📝 The single idea that unlocks FastAPI testing: **your app object is just a Python object you can call
directly.** You don't need a running server, a port, or a real HTTP socket to test an endpoint. FastAPI's
`TestClient` takes your `app`, sends a request *into it in-process*, and hands you back the response - all
inside the same Python process as your test.

That's why it's fast (no network, no process startup) and reliable (no "is the server up yet?"
flakiness). A test is just: build a client, call a route, check what came back - the same
Arrange-Act-Assert shape you'd use for any function; see [Your First Unit
Test](/guides/your-first-unit-test) for that shape from scratch.

## `TestClient`: calling your app like a function

`TestClient` comes from Starlette (the toolkit under FastAPI) and its API is modeled on the popular
`requests` library, so `.get()`, `.post()`, `.json()`, and `.status_code` all read the way you'd expect.

A complete, real test of the Book API. It needs the app object, so it's plain code (you can't run a
server inside the browser sandbox):

```python
# tests/test_books.py
from fastapi.testclient import TestClient
from app.main import app           # your FastAPI() instance

client = TestClient(app)

def test_list_books_returns_200_and_a_list():
    # Act: send a GET into the app, in-process
    response = client.get("/books")

    # Assert: status code AND the shape of the body
    assert response.status_code == 200
    assert isinstance(response.json(), list)

def test_create_book_returns_201_with_the_title():
    payload = {"title": "Dune", "author": "Frank Herbert"}
    response = client.post("/books", json=payload)

    assert response.status_code == 201
    body = response.json()
    assert body["title"] == "Dune"
    assert "id" in body          # the server assigned an id
```

*What just happened:* `TestClient(app)` wrapped your application so you can call it like an HTTP client
without any server running. `client.get("/books")` and `client.post("/books", json=...)` exercise the
*real* routing, the *real* Pydantic validation, the *real* response models - the whole stack from Phases
2–4 - and return a response object. You then assert on `status_code` and `response.json()`. We check more
than the status: a `201` with the wrong body is still a bug, so we assert the title round-trips and an
`id` was assigned. A genuine integration test, and it ran in milliseconds.

💡 Run these with `pytest` from your project root. It discovers any file named `test_*.py` and any
function named `test_*` inside it - no registration, no boilerplate:

```bash
pytest -q
```

A passing run looks like this:

```console
..                                                       [100%]
2 passed in 0.14s
```

## Dependency overrides: the testing superpower

Now the part that makes FastAPI testing genuinely special. [Phase 5](05-dependency-injection.md) promised
that because the database session, the current user, and everything else come in through `Depends()`,
your tests can *swap them out*. This is where you cash that promise.

📝 `app.dependency_overrides` is a dict that maps a dependency function to a replacement. When FastAPI is
about to call a dependency during a request, it checks this dict first - if there's an override, it calls
*that* instead. Your endpoint code doesn't change at all. It still asks for `get_session`; FastAPI just
quietly hands it the test double.

Two cases cover almost everything you'll ever need.

### Overriding the database with in-memory SQLite

You don't want tests hitting your real Postgres - slow, stateful, and one failing test can poison the
next. Point the session dependency at a fresh in-memory SQLite database that exists only for the test run:

```python
# tests/conftest.py
import pytest
from fastapi.testclient import TestClient
from sqlmodel import SQLModel, Session, create_engine
from sqlmodel.pool import StaticPool

from app.main import app
from app.db import get_session     # the real dependency from Phase 7

@pytest.fixture
def client():
    # one in-memory DB, shared across connections for this test
    engine = create_engine(
        "sqlite://",
        connect_args={"check_same_thread": False},
        poolclass=StaticPool,
    )
    SQLModel.metadata.create_all(engine)   # build the Book tables fresh

    def get_session_override():
        with Session(engine) as session:
            yield session

    # the swap: real session -> throwaway in-memory one
    app.dependency_overrides[get_session] = get_session_override

    yield TestClient(app)

    # teardown: undo the swap so the next test starts clean
    app.dependency_overrides.clear()
```

*What just happened:* The fixture stands up a brand-new SQLite database in memory, creates the Book
tables on it, and defines `get_session_override` - a `yield` dependency with the same shape as the real
one, but backed by the throwaway engine. The line `app.dependency_overrides[get_session] =
get_session_override` is the whole trick: every endpoint that does `Depends(get_session)` now gets the
test database, with zero changes to the endpoints. Each test that asks for the `client` fixture gets its
own pristine database, so tests can't contaminate each other.

⚠️ Look at that last line - `app.dependency_overrides.clear()`. **Overrides are global state on the app
object.** If you set one and don't reset it, it leaks into every test that runs afterward, and you get
the worst kind of bug: tests that pass or fail depending on what ran *before* them. Always undo overrides
in teardown; putting the override inside a fixture (which auto-runs its teardown after each test) is the
clean way to guarantee that.

### Overriding `get_current_user` to test protected routes

Protected endpoints from [Phase 8](08-authentication-and-security.md) are the other classic case. You
don't want to mint real JWTs in a test just to check `POST /books` works - you want to *pretend* you're
logged in:

```python
# tests/test_protected.py
from app.main import app
from app.dependencies import get_current_user

def fake_current_user():
    return {"username": "test-reader", "role": "admin"}

def test_create_book_when_authenticated(client):
    app.dependency_overrides[get_current_user] = fake_current_user
    try:
        response = client.post("/books", json={"title": "1984", "author": "Orwell"})
        assert response.status_code == 201
        assert response.json()["title"] == "1984"
    finally:
        app.dependency_overrides.pop(get_current_user, None)
```

*What just happened:* Instead of producing a valid token, you replaced the entire authentication
dependency with `fake_current_user`, which just returns a logged-in admin. The endpoint runs as if a real
user passed auth - no tokens, no password hashing, no headers to fake. The `try/finally` does the same job
as the fixture teardown above: it removes the override no matter what, so this test can't sabotage the
next one. This is how you test "what happens when an admin creates a book?" without dragging the whole
auth system into every test.

## The test pyramid, applied to this API

📝 Not every test should be an HTTP round-trip. The classic guidance - see
[Unit, Integration, E2E](/guides/unit-integration-e2e) - maps cleanly onto a FastAPI project:

- **Unit tests (the wide base).** Test pure functions and service logic *directly*, with no client and no
  app. If you have a `services.py` with `calculate_late_fee(book)` or `slugify_title(title)`, call it as a
  plain function and assert the result. Fastest and most numerous.
- **Integration tests (the middle).** Use `TestClient` to hit real endpoints against the in-memory test
  DB - what the examples above are. They prove routing, validation, the DB layer, and your response
  models all fit together. Most of your FastAPI tests live here.
- **End-to-end tests (the thin top).** A few tests against a *real* running server and a real (or
  containerized) database, exercising full flows like register → log in → create book → fetch it. Slow
  and more fragile, so keep them few and reserve them for critical paths.

💡 The practical rule of thumb: push logic down into plain functions you can unit-test, and use `TestClient`
for the seams where pieces meet. If you need an HTTP request just to test a calculation, that calculation
probably wants to be its own testable function.

## Project structure: escaping one giant `main.py`

⚠️ Every tutorial app starts as a single `main.py`, and that's fine - until it isn't. Once you have
books, authors, reviews, and auth, a 600-line `main.py` is where bugs hide and merge conflicts breed. You
can't find anything, and you can't test a slice of it in isolation.

📝 The fix is `APIRouter`. A router is a mini-FastAPI you can declare endpoints on, living in its own
module. You then *include* it into the main app - same routes, same behavior, just organized by feature.

A layout that scales without being over-engineered:

```text
app/
├── main.py            # creates FastAPI(), includes routers, app-wide config
├── db.py              # engine + get_session dependency (Phase 7)
├── models.py          # SQLModel Book, User, etc.
├── dependencies.py    # get_current_user and other shared dependencies
└── routers/
    ├── __init__.py
    ├── books.py       # everything under /books
    └── auth.py        # everything under /auth
tests/
├── conftest.py        # the client fixture + overrides
├── test_books.py
└── test_protected.py
```

A router module and the include look like this:

```python
# app/routers/books.py
from fastapi import APIRouter, Depends, status
from sqlmodel import Session

from app.db import get_session
from app.models import Book

router = APIRouter(prefix="/books", tags=["books"])

@router.get("")
def list_books(session: Session = Depends(get_session)):
    return session.query(Book).all()

@router.post("", status_code=status.HTTP_201_CREATED)
def create_book(book: Book, session: Session = Depends(get_session)):
    session.add(book)
    session.commit()
    session.refresh(book)
    return book
```

```python
# app/main.py
from fastapi import FastAPI
from app.routers import books, auth

app = FastAPI(title="Book API")

app.include_router(books.router)
app.include_router(auth.router)
```

*What just happened:* `APIRouter(prefix="/books", tags=["books"])` defines a self-contained set of routes;
the `prefix` means you write `@router.get("")` instead of repeating `/books` on every path, and the `tags`
group these endpoints together in `/docs`. In `main.py`, `app.include_router(books.router)` stitches the
router into the real application - at runtime the routes behave *identically* to having been declared on
`app` directly. The payoff: each feature lives in one file you can read, change, and test on its own, and
`main.py` shrinks to a short table of contents. (Remember from Phase 5 that `APIRouter` can also take
`dependencies=[...]` - that's how you require auth for every route in a router at once.)

## Settings, and the payoff

One last piece of structure. Hardcoding the database URL, secret key, or token expiry into your code is a
trap - different values in dev, test, and production, and secrets that must never be committed. The clean
answer is **pydantic-settings**: define a typed `Settings` model that reads from environment variables,
with the same validation you already trust from Pydantic.

```python
# app/config.py
from pydantic_settings import BaseSettings

class Settings(BaseSettings):
    database_url: str = "sqlite:///./books.db"
    secret_key: str
    access_token_expire_minutes: int = 30

    class Config:
        env_file = ".env"

settings = Settings()
```

*What just happened:* `Settings` pulls each field from an environment variable (or a `.env` file),
coerces and validates the types - `access_token_expire_minutes` *will* be an `int` or the app refuses to
start - and gives you one typed object to import. No more scattered `os.environ` lookups, no silently
wrong config. (You can even make `settings` a dependency and override it in tests, just like everything
else.)

💡 Step back and see the throughline of this whole guide. The reason all of this - swapping the test
database, faking the current user, splitting features into routers, overriding settings - is *easy* is
that nothing in your app reaches out and grabs its own dependencies. Everything is injected. Untestable
code is almost always un-injected code: a handler that constructs its own DB connection or reads
`os.environ` directly can't be tested without that real thing present. The layered, dependency-driven
design you've built isn't bureaucracy - it's exactly what makes the app testable, and exactly what makes
it ready for production. Which is where we go next.

## Recap

1. **`TestClient(app)`** calls your FastAPI app **in-process** - no server, no port, no network. It's
   built on Starlette with a `requests`-style API, so `client.get(...)`, `client.post(..., json=...)`,
   `.status_code`, and `.json()` are all you need. Run tests with `pytest`.
2. Assert on **more than the status code** - a `201` with the wrong body is still a bug. Check the shape
   and key fields of `response.json()`.
3. **`app.dependency_overrides`** swaps a real dependency for a test double: point `get_session` at an
   in-memory SQLite DB, or replace `get_current_user` to test protected routes without real auth. The
   endpoints don't change at all.
4. **Always reset overrides** (`.clear()` in a fixture teardown, or `try/finally`). They're global app
   state and leak between tests if you forget.
5. **Apply the test pyramid:** unit-test pure functions directly, use `TestClient` for endpoint
   (integration) tests, and keep a thin layer of real-DB end-to-end tests for critical flows.
6. **`APIRouter` + `app.include_router(...)`** splits one giant `main.py` into per-feature modules.
   **pydantic-settings** gives you typed config from the environment. Both exist because the whole design
   is dependency-driven - which is what makes the app testable in the first place.

## Quick check

Make sure the testing mechanics stuck before we ship to production:

```quiz
[
  {
    "q": "What does TestClient(app) actually do when you call client.get('/books')?",
    "choices": ["Starts a real HTTP server on a port and connects to it", "Sends the request into your app object in-process, no server needed", "Mocks out all your endpoints so nothing real runs", "Only works if `uvicorn` is already running in another terminal"],
    "answer": 1,
    "explain": "TestClient (from Starlette) wraps your app and dispatches requests directly into it in the same process. No port, no network, no running server - that's why it's fast and reliable."
  },
  {
    "q": "Why must you clear app.dependency_overrides after a test?",
    "choices": ["Otherwise pytest refuses to run the next file", "It frees memory FastAPI would otherwise leak", "Overrides are global state on the app, so a leftover one bleeds into later tests and makes them order-dependent", "Clearing it is what actually applies the override"],
    "answer": 2,
    "explain": "dependency_overrides is a dict living on the app object. If you don't reset it (via fixture teardown or try/finally), the override persists and silently affects every test that runs afterward."
  },
  {
    "q": "How do you split a large API into feature modules without changing endpoint behavior?",
    "choices": ["Define endpoints on an APIRouter in each module and app.include_router(...) them in main.py", "Copy main.py into several files and import whichever you need", "Run a separate FastAPI() per feature on different ports", "Move each endpoint into a Pydantic model"],
    "answer": 0,
    "explain": "APIRouter lets you declare routes in their own module (with a prefix and tags), then include_router stitches them into the app. At runtime the routes behave exactly as if declared on `app` directly."
  }
]
```


---

# Production & Where to Go Next

Take a second and look at what you can actually do now. You can stand up a FastAPI app, parse and validate requests straight from type hints, shape responses with Pydantic models, hide internal fields with response models, return correct status codes, inject dependencies for auth and database sessions with `Depends()`, persist data through SQLModel, lock endpoints down with OAuth2 and JWT, and prove the whole thing works with `TestClient` and pytest. That's not a toy. That's the shape of a real backend service - and the part that makes it *yours* is that you understand **why** each piece works. It all falls out of one idea: your **types are the contract**, and validation, docs, serialization, and DI are that one idea wearing different hats.

This last phase isn't more decorators - it's getting the thing onto the internet, a couple of patterns you'll reach for soon, the one async mistake that bites people in production, and a clear-eyed map of where to go next.

## Deploying it - uvicorn, workers, and a proxy

📝 In development you've been running `uvicorn main:app --reload`. That `--reload` flag and the single default worker are **dev-only** - reload watches your files and restarts on every save, which is wonderful locally and a liability in production. For real traffic you want **multiple worker processes** so requests run in parallel across CPU cores.

Two common ways to get there:

```bash
# Option A: uvicorn manages its own workers
uvicorn main:app --host 0.0.0.0 --port 8000 --workers 4

# Option B: gunicorn as the process manager, uvicorn workers underneath
gunicorn main:app -w 4 -k uvicorn.workers.UvicornWorker --bind 0.0.0.0:8000
```

Uvicorn is the **ASGI server** that actually speaks HTTP to your app; gunicorn is a battle-tested process manager that keeps a pool of uvicorn workers alive and restarts them if they die. Either works - gunicorn is the traditional choice when you want robust process supervision.

⚠️ You don't expose that directly to the world. In front of it goes a **reverse proxy** like nginx, handling TLS, serving static files, buffering slow clients, and forwarding the rest to your workers. And the cleanest way to ship the whole bundle is to **containerize it with Docker**, so the same image runs on your laptop and your server.

This is exactly the territory two other guides cover in depth: [Ship Your Side Project](/guides/ship-your-side-project) walks the deployment path end to end, and [Docker Without the Magic](/guides/docker-without-the-magic) demystifies the container part so it stops feeling like incantations.

## Background tasks and beyond

Sometimes you want to do a little work *after* the response goes out - send a welcome email, write an audit log line - without making the user wait for it. FastAPI has a built-in tool for that:

```python runnable
# Conceptually, this is what BackgroundTasks does:
def send_welcome_email(address: str):
    print(f"(later) sent welcome email to {address}")

def signup(address: str, queue: list):
    queue.append((send_welcome_email, address))
    return {"status": "created"}

# FastAPI returns the response, THEN runs queued tasks:
tasks = []
print(signup("ada@example.com", tasks))
for fn, arg in tasks:
    fn(arg)
```

📝 In real FastAPI you write `def signup(background_tasks: BackgroundTasks)` and call `background_tasks.add_task(send_welcome_email, address)`. It's perfect for **light, fire-and-forget** work that runs in the same process after responding.

It is **not** for heavy lifting. If the work is slow, needs to be retried on failure, must survive a restart, or runs on a schedule, you want a real **external worker**: Celery, RQ, or Dramatiq, fronted by a **message broker** (Redis or RabbitMQ). Your API drops a job on the broker and returns immediately; a separate worker process picks it up.

```mermaid
flowchart TD
  Req[Incoming request] --> App[FastAPI handler]
  App -->|light, after response| BG[BackgroundTasks]
  App -->|heavy / retryable / scheduled| Broker[Broker: Redis or RabbitMQ]
  Broker --> Worker[Celery / RQ / Dramatiq]
```

*What this shows:* two clear branches. Trivial follow-up work stays in-process with `BackgroundTasks`; anything that needs reliability or muscle goes to an external worker through a broker.

A few more doors, one line each: **WebSockets** let you hold an open two-way connection for live updates; **streaming responses** let you send a body in chunks instead of all at once; and **middleware** wraps every request and response, the right home for cross-cutting concerns like logging or custom headers.

## The async pitfalls, in one place

⚠️ Here's the one that actually bites in production, straight from Phase 6: **don't block the event loop.** When you write `async def`, your handler shares a single thread with every other in-flight request. Call a slow *blocking* function inside it - a synchronous database driver, `time.sleep()`, a heavy CPU loop - and you don't slow down one request, you freeze *all* of them at once.

The rules that keep you out of trouble:

- Use `async def` when you `await` genuinely async libraries. Use a plain `def` for ordinary blocking code - FastAPI runs `def` handlers in a threadpool so they can't stall the loop.
- If you go full-async, pair it with an **async database driver** (async SQLAlchemy, `asyncpg`). An `async def` handler calling a *blocking* DB call is the worst of both worlds.

💡 The vast majority of FastAPI performance complaints come down to one of two things: a **blocked event loop**, or an **N+1 query** quietly firing one database call per row. Neither is a framework flaw - both are fixable once you know to look for them.

## Where to go next - a clear-eyed framework map

FastAPI is a sharp tool for a specific job. It's the right pick for **APIs, microservices, and serving ML models** behind an endpoint - anywhere you want a fast, typed, well-documented interface. But it isn't the only Python web framework, and pretending it's always the answer would be misleading:

- **Django** when you want **batteries included** - an admin panel, a mature ORM, templating, auth, and a thousand conventions - for a full web application, not just an API. If you'd otherwise rebuild half of Django by hand, use Django.
- **Flask** for something **tiny and simple** - a small service or a quick prototype where FastAPI's machinery is more than you need.

(Each of those has its own guide when you're ready.)

As for what to build: take the **book API** you've been growing through this guide and carry it all the way home. Add real authentication, a real database, a real test suite, wrap it in Docker, and **deploy it** somewhere you can hit it from your phone. That single project exercises nearly everything you learned. Or point FastAPI at a different problem entirely - load a trained ML model on startup and **serve predictions** behind a clean, typed endpoint. That's one of the things FastAPI does best.

When you want the canonical reference, the **official FastAPI documentation and tutorial** are genuinely excellent - clear, example-driven, and maintained by the people who build it. Bookmark them.

And remember the through-line: none of this was magic. The validation, the docs, the serialization, the dependency injection - every "it just works" came from one place. The magic was your **type hints** all along. You can read what's underneath now, build a real service on top, and reason about it when it breaks. Go finish the book API, deploy it, and show someone. You're ready.

## Recap

1. **You can build and ship a real FastAPI service** - validated, authenticated, tested, database-backed - and you understand *why* each layer works, because types are the contract underneath all of it.
2. **Deploy with an ASGI server and workers** - `uvicorn --workers` or gunicorn with uvicorn workers, behind a reverse proxy like nginx, packaged in Docker. `--reload` and a single worker are dev-only.
3. **Pick the right tool for background work** - `BackgroundTasks` for light fire-and-forget after the response; an external worker (Celery / RQ / Dramatiq) plus a broker for heavy, retryable, or scheduled jobs.
4. **Don't block the event loop** - match `async def` vs `def` to your code, use an async DB driver if you go full-async, and remember most perf problems are a blocked loop or an N+1.
5. **Know the framework map** - FastAPI for APIs/microservices/ML-serving, Django when you want batteries-included for a full app, Flask for tiny things.
6. **Build one thing and finish it** - carry the book API to a deployed, authenticated, tested, Dockerized service, or serve an ML model behind FastAPI. The magic was your type hints all along.

## Quick check

Test yourself on the decisions that matter most as you leave this guide:

```quiz
[
  {
    "q": "Why are `--reload` and a single uvicorn worker considered dev-only?",
    "choices": [
      "Reload restarts on file changes and one worker can't use multiple cores - production wants stable, multi-worker processes",
      "They are deprecated and removed in recent FastAPI versions",
      "They disable automatic docs, which production needs",
      "They only work on Windows"
    ],
    "answer": 0,
    "explain": "Reload watches your files and restarts on every save - great locally, a liability in production. For real traffic you run multiple workers (uvicorn --workers, or gunicorn with uvicorn workers) behind a reverse proxy."
  },
  {
    "q": "You need to send a heavy report email that must be retried if it fails and survive a restart. What fits best?",
    "choices": [
      "FastAPI's BackgroundTasks, since it runs after the response",
      "An external worker (Celery / RQ / Dramatiq) backed by a broker like Redis or RabbitMQ",
      "An async def handler that awaits the email send inline",
      "A WebSocket connection to the mail server"
    ],
    "answer": 1,
    "explain": "BackgroundTasks is for light, fire-and-forget work in the same process. Anything heavy, retryable, scheduled, or that must survive a restart belongs on an external worker fronted by a broker."
  },
  {
    "q": "What is the classic async mistake that freezes a whole FastAPI app under load?",
    "choices": [
      "Calling a slow, blocking function inside an async def handler, which stalls the shared event loop for every request",
      "Using too many Pydantic models in one endpoint",
      "Returning a response model instead of a dict",
      "Adding a reverse proxy in front of uvicorn"
    ],
    "answer": 0,
    "explain": "An async def handler shares one thread with all in-flight requests. A blocking call inside it freezes them all. Use plain def for blocking code (FastAPI threadpools it), and an async DB driver if you go full-async."
  }
]
```
