In this post
What this post covers
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 part 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 guide 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.
1. Install
in a fresh virtual environment (Core Python part 02)
uv add "fastapi[standard]" # or: python -m pip install "fastapi[standard]"
| Package | What you get |
|---|---|
fastapi | the 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 uvicorn | when 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 parts show.
2. Run it: the CLI or uvicorn
main.py
from fastapi import FastAPI
app = FastAPI(title="Hello API", version="0.1.0")
@app.get("/")
def root():
return {"message": "hello"}
two ways to serve it
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:
a real server, a real HTTP request
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)))
Output
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 (part 20).
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. Part 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 guide uses it:
the same checks, without a server
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)
Output
200 {'message': 'hello'} | docs: 200 | missing: 404
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 guide filter the warning so the outputs stay readable.
5. A starting layout
small enough to start, shaped to grow (part 07 expands it)
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
| Tool | Why, for FastAPI specifically |
|---|---|
| ruff (lint + format) | catches unused imports, undefined names and dozens of bug patterns; formats on save |
| mypy or pyright | FastAPI is built on type hints, so a type checker verifies the same hints that drive validation |
pytest + TestClient | every endpoint tested in-process (part 19) |
| pre-commit and CI | run all of the above on every commit and pull request |
| your editor's debugger | run 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 devwhile coding,fastapi runoruvicorn module:appin production. - /docs, /redoc, /openapi.json come free. Decide deliberately whether production exposes them.
- TestClient exercises the app in-process: no server, instant feedback.
Get the free RAG production checklist
25 checks to run before real users see your RAG feature: exact-match misses, invented citations, prompt injection through documents, quality that slips after a prompt change. Confirm your email and the PDF comes with the welcome email, plus each new experiment.