Module 1 Β· How FastAPI works

Setup, tooling & the dev loop

Foundations 16 min read Install, run, explore /docs, and test in-process from day one

A productive FastAPI setup is small: a virtual environment, fastapi with a server, auto-reload while you code, the interactive docs at /docs, and a test client so every endpoint is tested in-process without starting a server. This lesson sets that up, runs a real uvicorn server to show what happens on the wire, and introduces the in-process testing style the rest of the course relies on.

πŸ§ͺ

A kitchen with a tasting spoon

uvicorn is the restaurant's front door: real customers, real traffic. The test client is the chef's tasting spoon: you check each dish in the kitchen, instantly, without seating a customer. Good teams taste constantly and open the doors only when the dish is right.

uvicorn: the server process sockets Β· HTTP parsing Β· event loop Β· --reload Β· --workers FastAPI app (built on Starlette) routing Β· middleware Β· validation Β· dependencies Β· /docs your path operations plain def / async def functions bytes on a socket become an ASGI scope and receive/send calls validated arguments

1. Install

uv add "fastapi[standard]"           # or: python -m pip install "fastapi[standard]"
PackageWhat you get
fastapithe framework, with Starlette and Pydantic as dependencies
fastapi[standard]plus uvicorn, the fastapi CLI, httpx (test client), python-multipart (forms and uploads), email-validator, Jinja2
minimal: fastapi uvicornwhen you want to choose every dependency yourself (production images often do)

FastAPI needs Python 3.10+ (since 0.129). Pin versions for services; FastAPI is pre-1.0 and minor releases do sometimes change behaviour, as later lessons show.

2. Run it: the CLI or uvicorn

from fastapi import FastAPI

app = FastAPI(title="Hello API", version="0.1.0")

@app.get("/")
def root():
    return {"message": "hello"}
fastapi dev main.py                      # development: auto-reload, binds to 127.0.0.1:8000
fastapi run main.py                      # production defaults: no reload, binds to 0.0.0.0

uvicorn main:app --reload                # the same, with uvicorn directly ("module:attribute")
uvicorn main:app --host 0.0.0.0 --port 8000 --workers 4

The CLI comes with fastapi[standard] and wraps uvicorn with good defaults. This example runs a real uvicorn server in a subprocess, talks to it over a real socket, and prints its logs:

import re
import socket
import subprocess
import sys
import time
from pathlib import Path

import httpx

Path("main.py").write_text(
    "from fastapi import FastAPI\n"
    "app = FastAPI(title='Hello API', version='0.1.0')\n\n"
    "@app.get('/')\n"
    "def root():\n"
    "    return {'message': 'hello'}\n", encoding="utf-8")

with socket.socket() as s:                         # find a free port
    s.bind(("127.0.0.1", 0))
    port = s.getsockname()[1]

server = subprocess.Popen([sys.executable, "-m", "uvicorn", "main:app", "--port", str(port)],
                          stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True)
try:
    base = f"http://127.0.0.1:{port}"
    for _ in range(100):                           # wait until it accepts connections
        try:
            r = httpx.get(base + "/")
            break
        except httpx.ConnectError:
            time.sleep(0.1)
    print(r.status_code, r.headers["content-type"], r.json())
    print("/docs serves Swagger UI:", "swagger-ui" in httpx.get(base + "/docs").text)
    print("/openapi.json info:", httpx.get(base + "/openapi.json").json()["info"])
    print("unknown path:", httpx.get(base + "/nope").status_code)
finally:
    server.terminate()
    logs = server.communicate(timeout=10)[0]
for line in logs.splitlines():
    if line.startswith("INFO") and "Uvicorn running" not in line:
        print("  log |", re.sub(r"\[\d+\]", "[pid]", re.sub(r":\d{4,5}", ":PORT", line)))
200 application/json {'message': 'hello'} /docs serves Swagger UI: True /openapi.json info: {'title': 'Hello API', 'version': '0.1.0'} unknown path: 404 log | INFO: Started server process [pid] log | INFO: Waiting for application startup. log | INFO: Application startup complete. log | INFO: 127.0.0.1:PORT - "GET / HTTP/1.1" 200 OK log | INFO: 127.0.0.1:PORT - "GET /docs HTTP/1.1" 200 OK log | INFO: 127.0.0.1:PORT - "GET /openapi.json HTTP/1.1" 200 OK log | INFO: 127.0.0.1:PORT - "GET /nope HTTP/1.1" 404 Not Found

3. The interactive docs

/docs

Swagger UI: every endpoint, its parameters and schemas, with a "Try it out" button that sends real requests.

/redoc

ReDoc: a clean, read-only reference, good for sharing with API consumers.

/openapi.json

The machine-readable contract. Clients, SDK generators, API gateways and contract tests all consume it (lesson 20).

In production

Public docs are a gift to attackers mapping your API. For internal services, keep them. For public ones, either disable them (FastAPI(docs_url=None, redoc_url=None, openapi_url=None)) or serve them behind authentication. Lesson 20 shows both.

4. Test in-process from day one

TestClient calls your ASGI app directly, with no server and no port, and returns ordinary response objects. It is the fastest feedback loop you have, and nearly every example in this course uses it:

from fastapi import FastAPI
from fastapi.testclient import TestClient

app = FastAPI(title="Hello API", version="0.1.0")

@app.get("/")
def root():
    return {"message": "hello"}

client = TestClient(app)
r = client.get("/")
assert r.status_code == 200 and r.json() == {"message": "hello"}
print(r.status_code, r.json(), "| docs:", client.get("/docs").status_code, "| missing:", client.get("/nope").status_code)
200 {'message': 'hello'} | docs: 200 | missing: 404
The httpx2 warning

With Starlette 1.x, importing TestClient prints a deprecation warning suggesting httpx2, the next generation of the httpx client it is built on. It is harmless. Install httpx2 in your dev dependencies to silence it (FastAPI's own test suite did this in 0.137). The examples in this course filter the warning so the outputs stay readable.

5. A starting layout

orders-api/
β”œβ”€β”€ pyproject.toml
β”œβ”€β”€ app/
β”‚   β”œβ”€β”€ __init__.py
β”‚   β”œβ”€β”€ main.py            ← create_app(): builds the FastAPI instance
β”‚   β”œβ”€β”€ settings.py        ← pydantic-settings (lesson 08)
β”‚   β”œβ”€β”€ api/               ← routers, one per resource
β”‚   β”œβ”€β”€ services/          ← business logic, no HTTP in here
β”‚   └── db/                ← models, sessions, repositories
└── tests/
    └── test_orders.py

6. Tooling that pays for itself

ToolWhy, for FastAPI specifically
ruff (lint + format)catches unused imports, undefined names and dozens of bug patterns; formats on save
mypy or pyrightFastAPI is built on type hints, so a type checker verifies the same hints that drive validation
pytest + TestClientevery endpoint tested in-process (lesson 19)
pre-commit and CIrun all of the above on every commit and pull request
your editor's debuggerrun uvicorn.run("main:app") from a small script, or use the editor's FastAPI launch configuration, and set breakpoints in endpoints

Recap

  • Install fastapi[standard] in a venv; Python 3.10+; pin versions.
  • Run with fastapi dev while coding, fastapi run or uvicorn module:app in production.
  • /docs, /redoc, /openapi.json come free. Decide deliberately whether production exposes them.
  • TestClient exercises the app in-process: no server, instant feedback.

Checkpoint

1 Β· What does uvicorn main:app refer to?
uvicorn imports the module, then takes the attribute after the colon: your ASGI application object.
2 Β· Why prefer TestClient over running a server for most tests?
The app is just a callable. Calling it directly exercises the same routing, validation and serialization, with far less friction.