What FastAPI really is
FastAPI is a thin, clever layer on top of two other libraries. Starlette provides the web toolkit: routing, requests, responses, middleware, WebSockets. Pydantic provides data validation and serialization. FastAPI's own contribution is to read your function's type hints and wire those two together: parse and validate the request, inject dependencies, serialize the result, and describe all of it as an OpenAPI schema. Knowing where each layer starts and stops is what lets you debug FastAPI instead of guessing.
A hotel front desk
The building and corridors are Starlette: rooms (routes), doors (middleware), a lift (the ASGI server). The strict receptionist who checks every guest's ID and booking against a form is Pydantic. FastAPI is the hotel manager who wrote the booking forms from your descriptions of each room, so arriving guests are checked exactly against what the room expects, and the brochure (OpenAPI) always matches the rooms.
1. ASGI: the contract between server and app
Python web apps run behind a server (uvicorn, hypercorn, granian) that handles sockets and HTTP
parsing. The old contract, WSGI, was one synchronous function call per request, so a worker was stuck
until the response was done. ASGI is its asynchronous successor. The app is an async
callable taking a scope (what this connection is) and two channels, receive and
send. One process can therefore juggle thousands of connections, and long-lived ones such as
WebSockets and streams fit naturally.
No framework is needed to speak ASGI. Here is a complete ASGI application, driven by httpx's in-process ASGI transport so you can see every message:
import asyncio
import json
import httpx
async def app(scope, receive, send):
"""The whole ASGI contract: read the scope, receive the body, send start + body."""
assert scope["type"] == "http"
message = await receive() # {"type": "http.request", "body": b"..."}
payload = {"method": scope["method"], "path": scope["path"],
"query": scope["query_string"].decode(), "body": message["body"].decode()}
body = json.dumps(payload).encode()
await send({"type": "http.response.start", "status": 200,
"headers": [(b"content-type", b"application/json")]})
await send({"type": "http.response.body", "body": body})
async def main():
transport = httpx.ASGITransport(app=app) # calls app() in-process, no network
async with httpx.AsyncClient(transport=transport, base_url="http://test") as client:
r = await client.post("/orders?dry_run=1", content=b'{"sku": "TEA-01"}')
print(r.status_code, r.headers["content-type"])
print(r.json())
asyncio.run(main())
Everything above that contract (routing, parsing JSON, validation, error pages, OpenAPI) is what Starlette
and FastAPI give you. In production, uvicorn main:app plays the role httpx played here.
2. The three layers, and what each one owns
| Layer | Owns | You meet it as |
|---|---|---|
| Starlette (web toolkit) | routing, Request/Response, middleware, WebSockets, background tasks, static files, the test client, lifespan | from starlette… (FastAPI re-exports most of it: fastapi.Request, fastapi.responses) |
| Pydantic (data) | parsing and validating input, serializing output, JSON Schema | BaseModel, Field, validators, pydantic-settings |
| FastAPI (glue) | reading type hints, dependency injection, request → Pydantic → your function → response model, OpenAPI generation, security utilities | FastAPI, APIRouter, Depends, Query, HTTPException |
3. The same idea, with FastAPI
One function, two type hints, and FastAPI parses the path parameter as an int, validates the query parameter, returns 422 with a precise error when input is wrong, serializes the dict to JSON, and documents all of it:
from typing import Annotated
from fastapi import FastAPI, Query
from fastapi.testclient import TestClient
app = FastAPI(title="Shop")
@app.get("/orders/{order_id}")
def read_order(order_id: int, expand: Annotated[bool, Query()] = False):
return {"order_id": order_id, "expanded": expand}
client = TestClient(app)
print(client.get("/orders/42?expand=true").json()) # "42" → 42, "true" → True
r = client.get("/orders/forty-two")
print(r.status_code, r.json()["detail"][0]["msg"]) # validated before your code runs
schema = client.get("/openapi.json").json()
params = schema["paths"]["/orders/{order_id}"]["get"]["parameters"]
print([(p["name"], p["in"], p["schema"]["type"]) for p in params]) # docs generated from the hints
4. A FastAPI app is a Starlette app
from fastapi import FastAPI
from starlette.applications import Starlette
app = FastAPI()
@app.get("/health")
def health():
return {"ok": True}
print(isinstance(app, Starlette))
for route in app.routes: # yours, plus the built-in docs routes
print(f"{type(route).__name__:10} {route.path:24} {sorted(getattr(route, 'methods', []) or [])}")
Anything that works with Starlette works with FastAPI: its middleware, its Request object, its
test client, even mounting a whole Starlette or ASGI app under a path. That is why this course keeps
pointing out which layer a behaviour comes from. The fix is usually in that layer's documentation.
5. When FastAPI is, and is not, the right tool
| Good fit | Think twice |
|---|---|
| JSON APIs and microservices, especially with typed, validated contracts | A content-heavy website with admin screens, forms and an ORM out of the box: Django gives you those batteries |
| I/O-bound work: databases, other APIs, LLM calls, streaming | CPU-bound work per request (image processing, heavy maths): offload it to workers (lesson 11) whatever framework you use |
| Teams that want OpenAPI docs and generated clients for free | Very small scripts where a framework adds nothing |
| AI back ends: model gateways, RAG services, MCP servers (lesson 14's SSE) | Teams unwilling to learn async. Lesson 11 shows why misused async is worse than none. |
"Fast" refers to two things. Development speed: type hints give you validation, docs and editor
support in one place. Runtime speed: the ASGI model handles many concurrent I/O-bound requests per
process, and since FastAPI 0.130 responses with a declared return type are serialized by Pydantic's Rust
core. Your code still decides the rest: one blocking call inside async def can erase all of it
(lesson 11).
Recap
- ASGI: the app is
async (scope, receive, send); the server (uvicorn) handles sockets and HTTP. - Starlette = web toolkit, Pydantic = data, FastAPI = type-hint glue + dependency injection + OpenAPI.
- Type hints drive everything: parsing, validation, 422 errors, serialization and documentation.
- A FastAPI app is a Starlette app, so debug in the layer that owns the behaviour.
Checkpoint
order_id is "forty-two"?