Module 2 · FastAPI · Deep Dive

FastAPI Core

Your Python is fluent — now turn it into a web API. FastAPI is how DocChat will talk to the world: typed routes, automatic validation, and docs you get for free.

BasicIntermediateBuild

Why this matters DocChat needs a backend that accepts uploads, lists documents, answers questions, and returns clean JSON. That backend is FastAPI — the framework UAE employers list next to "Python" on nearly every job post in 2026. You already know HTTP from PHP. This lesson maps that knowledge onto FastAPI's type-driven style, then has you build a real CRUD API you'll keep extending all module.
In this lesson
  1. What FastAPI is & why
  2. Install, run, and the free docs
  3. Path operations & path params
  4. Query params & defaults
  5. Pydantic models for request bodies
  6. JSON, status codes & errors
  7. async def vs def
  8. Build: DocChat documents API
  9. Check yourself

1 · What FastAPI is & why

FastAPI is a Python web framework for building APIs. Three things make it the modern default — and each one is an interview talking point:

PHP bridge: think Laravel routing, but the route's type hints are the validation layer. You don't write a separate Request validator — the function signature is the contract.

2 · Install, run, and the free docs

Install FastAPI with its standard extras (this pulls in the dev server, Uvicorn, and the validation engine):

# the quotes matter — they stop your shell eating the brackets
pip install "fastapi[standard]"

Write the smallest possible app in main.py:

main.py
from fastapi import FastAPI

app = FastAPI()

@app.get("/")
def root():
    return {"status": "ok"}

Run it with the dev server, which reloads on every save:

fastapi dev main.py

Now open the two URLs you got for free:

Use /docs constantly You almost never need Postman or curl while developing FastAPI. The /docs page is a live test client generated from your type hints. Build a route, refresh, click Execute. This is the single biggest productivity win over your old PHP workflow.

3 · Path operations & path params

A path operation is a route: an HTTP method plus a URL, declared with a decorator on a function. The four you'll use most:

@app.get("/documents")      # read a collection
@app.post("/documents")     # create
@app.put("/documents/{id}") # replace/update
@app.delete("/documents/{id}")

A path parameter is a piece of the URL you capture. Put it in braces in the path, then declare it as a function argument with a type hint. FastAPI validates and converts it for you:

@app.get("/documents/{doc_id}")
def get_document(doc_id: int):
    return {"doc_id": doc_id}

Because doc_id is typed int, a request to /documents/7 arrives as the integer 7. A request to /documents/abc never reaches your code — FastAPI returns a clean 422 validation error automatically.

PHP bridge: in Laravel you'd write Route::get('/documents/{id}', ...) then cast and check $id yourself. Here the : int hint does the cast and the rejection — for free.

4 · Query params & defaults

Function arguments that aren't in the path automatically become query parameters (the ?key=value part of a URL). Give them a default to make them optional:

from typing import Optional

@app.get("/documents")
def list_documents(limit: int = 10, search: Optional[str] = None):
    return {"limit": limit, "search": search}

Now /documents uses limit=10 and no search; /documents?limit=5&search=tax overrides both. Optional[str] = None means "a string if given, otherwise nothing" — the modern way to say "this parameter is optional".

The rule of thumb In the path → path param (identifies which resource). After the ?query param (filters, sorts, paginates the resource). /documents/7 vs /documents?limit=5.

5 · Pydantic models for request bodies

For anything more than a couple of fields — like the JSON a client POSTs to create a document — you declare a Pydantic model: a class that inherits from BaseModel with typed fields. FastAPI reads, validates, and parses the request body against it.

from pydantic import BaseModel, Field

class Document(BaseModel):
    title: str = Field(..., min_length=1)
    pages: int = Field(..., gt=0)

@app.post("/documents")
def create_document(doc: Document):
    return doc

Because doc is typed as a Document, FastAPI knows it comes from the request body. Send {"title": "Intro", "pages": 12} and you get a validated Document object. Send {"pages": -1} and the client gets a precise 422 error explaining what's wrong — you wrote zero validation code.

Field(...) adds constraints. The literal ... means "required". Common ones:

ConstraintMeaningApplies to
min_length=1Non-empty / minimum sizestrings, lists
max_length=200Maximum sizestrings, lists
gt=0Strictly greater thannumbers
ge=1Greater than or equalnumbers
PHP bridge: this is like validating $_POST against a rules array — but automatic, typed, and self-documenting. The model is the rulebook, the parser, and the OpenAPI schema all at once. Python bridge: remember @dataclass from Module 1? Pydantic's BaseModel is that idea — a typed data holder — plus validation, coercion, and JSON serialisation baked in.

response_model shaping what goes out

You can also declare what a route returns with response_model. FastAPI filters the output to match — handy for hiding internal fields like a password hash:

@app.post("/documents", response_model=Document)
def create_document(doc: Document):
    return doc   # output validated & shaped to Document

nested models models inside models

A field can itself be another model — exactly how real JSON nests:

class Author(BaseModel):
    name: str

class Document(BaseModel):
    title: str
    author: Author   # nested — {"title": ..., "author": {"name": ...}}

6 · JSON, status codes & errors

Return a dict (or a Pydantic model, or a list) and FastAPI serialises it to JSON with the right headers. No json_encode, no Content-Type juggling.

@app.get("/health")
def health():
    return {"ok": True}   # → {"ok": true} with 200

Set a non-default status code on the decorator — a successful create should answer 201 Created:

@app.post("/documents", status_code=201)
def create_document(doc: Document):
    return doc

When something genuinely goes wrong — a document that doesn't exist — raise an HTTPException. FastAPI turns it into the right JSON error response:

from fastapi import HTTPException

@app.get("/documents/{doc_id}")
def get_document(doc_id: int):
    if doc_id not in store:
        raise HTTPException(404, "Document not found")
    return store[doc_id]
PHP bridge: instead of http_response_code(404); echo json_encode([...]), you raise and let the framework format the response. Cleaner, and it short-circuits the function.

7 · async def vs def

You can write a path function as either def or async def, and FastAPI handles both. Use async def when the body awaits something I/O-bound — a database query, an HTTP call to an LLM, a file read. Use plain def for quick, synchronous, CPU-light work; FastAPI runs it safely in a threadpool so it never blocks the event loop. The rule: if a library gives you an await-able call, make the route async def and await it; otherwise plain def is fine.

@app.get("/documents/{doc_id}")
async def get_document(doc_id: int):
    doc = await db.fetch(doc_id)   # await an I/O call
    return doc
Interview hook — "why async?" Expect "Why is FastAPI async?" The crisp answer: web work is mostly waiting on I/O (DB, network, LLMs). An async event loop lets one worker handle many requests by switching to other work while it waits, instead of blocking a whole thread per request. That's higher throughput on the same hardware — which is exactly what DocChat needs when it's waiting on a slow model call.

8 · Build it

Your tangible win Build the DocChat documents API — an in-memory CRUD service with four routes: list all documents, get one by id, create one, delete one. It uses a Pydantic Document model and raises a proper 404. This is the literal foundation you'll bolt a database and RAG onto in later modules.

Run it with fastapi dev main.py, open /docs, and exercise every route from the browser.

main.py
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel, Field

app = FastAPI()

class Document(BaseModel):
    title: str = Field(..., min_length=1)
    pages: int = Field(..., gt=0)

documents: dict[int, Document] = {}   # in-memory store
next_id = 1

@app.get("/documents")
def list_documents():
    return documents

@app.get("/documents/{doc_id}")
def get_document(doc_id: int):
    if doc_id not in documents:
        raise HTTPException(404, "Document not found")
    return documents[doc_id]

@app.post("/documents", status_code=201)
def create_document(doc: Document):
    global next_id
    documents[next_id] = doc
    new_id, next_id = next_id, next_id + 1
    return {"id": new_id, "document": doc}

@app.delete("/documents/{doc_id}", status_code=204)
def delete_document(doc_id: int):
    if doc_id not in documents:
        raise HTTPException(404, "Document not found")
    del documents[doc_id]

That's a complete, validated, self-documented REST API in ~25 lines. The next lesson adds the missing update (PUT) route, then we replace the in-memory dict with a real database.

9 · Check yourself

Answer from memory — retrieval is what moves this from "I read it" to "I know it".

Recall quiz

Where do FastAPI's interactive Swagger docs live?

What validates an incoming JSON request body?

How do you return a clean 404 from a route?

In /documents/{doc_id}, what is doc_id?

When is async def the right choice for a route?

Primary source ⭐ FastAPI — Learn. The official, authoritative tutorial covering everything above (path params, query params, request bodies, response models, error handling). Work through it alongside these lessons — it's exceptionally well written.