FastAPI Introduction
FastAPI is a Python web framework for building HTTP APIs with ordinary Python functions, type hints, Starlette’s ASGI machinery, and Pydantic data models. The outcome of this introduction is concrete: you should be able to explain what happens when a client sends a request, where FastAPI reads the path, query string, headers, and body, how it decides which function to call, why invalid input becomes a structured error response, and how the returned Python object becomes JSON.
In this foundations course, that request path matters because every later FastAPI feature builds on it. Routers, dependencies, security schemes, background tasks, response models, and tests are not separate tricks. They are extensions of the same lifecycle: receive an ASGI event, match an operation, prepare arguments, call your endpoint, and send a response.
Purpose and Request Outcome
A minimal FastAPI application maps an HTTP method and path to a callable. The callable can be synchronous or asynchronous. FastAPI inspects the callable signature and annotations to decide where each argument should come from. A path variable comes from the URL path, a simple scalar may come from the query string, a Pydantic model usually comes from the request body, and a dependency is produced by another callable.
The simplest deterministic outcome is a JSON response. In this example, GET /health returns a small object and HTTP status 200. FastAPI turns the returned dictionary into a JSON response through Starlette’s response handling.
from fastapi import FastAPI
app = FastAPI(title="Inventory API")
@app.get("/health")
def health_check():
return {"status": "ok"}
If this app is saved as main.py and served with uvicorn main:app --reload, a request to /health returns {"status":"ok"}. A request to an unknown path returns a 404 response because routing happens before your endpoint function is considered.
ASGI and FastAPI Internals
FastAPI applications are ASGI applications. ASGI is a callable protocol used by servers such as Uvicorn. For an HTTP request, the server gives the application a connection scope describing method, path, headers, client address, and protocol details. It also provides receive and send callables for request and response events. FastAPI delegates much of this low-level handling to Starlette.
The request lifecycle has several important stages. First, middleware wraps the application. Middleware can observe or modify requests before routing and responses after the endpoint runs. Next, Starlette’s router compares the HTTP method and normalized path against registered routes. If no route matches, it returns a 404; if the path matches but the method does not, it returns a 405.
After a route is selected, FastAPI analyzes the endpoint function. It builds a dependency graph from parameters marked with Depends, security utilities, and request parameter declarations. It parses path parameters, query parameters, headers, cookies, and body data into Python values. Pydantic validates model fields, applies type conversion where appropriate, and reports precise locations for invalid data. Only after this preparation does FastAPI call your endpoint.
The endpoint return value then moves through serialization. If a response_model is declared, FastAPI validates and filters the outgoing data against that model. This is why internal fields can be excluded from public responses. The value is encoded into JSON-compatible data, wrapped in a response object, and sent back through middleware to the ASGI server.
API Anatomy
A path operation decorator has three core pieces: the HTTP method, the path template, and optional metadata or behavior. The function signature is the API contract. Parameters such as item_id: int in the path become typed path parameters. Parameters declared with Query, Path, Header, or Cookie add validation rules and OpenAPI documentation. Pydantic models describe JSON bodies. Status codes, response models, tags, and summaries refine the public contract.
The next example combines path, query, and body data. It also demonstrates a deterministic validation failure: sending a quantity below 1 returns HTTP 422 with a validation error before the endpoint body runs.
from fastapi import FastAPI, Query
from pydantic import BaseModel, Field
app = FastAPI(title="Inventory API")
class StockChange(BaseModel):
sku: str = Field(min_length=3, max_length=20)
quantity: int = Field(ge=1, le=500)
@app.post("/warehouses/{warehouse_id}/stock")
def add_stock(
warehouse_id: int,
change: StockChange,
notify: bool = Query(default=False),
):
return {
"warehouse_id": warehouse_id,
"sku": change.sku.upper(),
"accepted_quantity": change.quantity,
"notification_requested": notify,
}
A request to POST /warehouses/7/stock?notify=true with body {"sku":"abc123","quantity":4} returns warehouse id 7, uppercased SKU ABC123, accepted quantity 4, and notification flag true. A body with {"sku":"ab","quantity":0} fails because the SKU is too short and quantity is below the declared minimum. FastAPI reports both problems with field locations.
Dependencies, Responses, and Middleware
Dependencies are callables FastAPI resolves before the endpoint. They are useful for shared inputs such as authentication context, database sessions, settings, or request-scoped services. A dependency may depend on other dependencies, so FastAPI builds and resolves a graph. Values are cached within a single request by default, which prevents repeated work when multiple parameters need the same dependency result.
Response models provide a separate public shape for output. This is a design choice: input models describe what clients may send, while response models describe what clients may receive. They do not have to be the same. Middleware surrounds the whole route operation and is the right place for cross-cutting request behavior such as timing headers or correlation ids.
import time
from fastapi import Depends, FastAPI, Header, HTTPException, Request
from pydantic import BaseModel
app = FastAPI(title="Inventory API")
class UserContext(BaseModel):
username: str
role: str
class ItemOut(BaseModel):
sku: str
name: str
visible_to: str
async def current_user(x_user: str | None = Header(default=None)) -> UserContext:
if not x_user:
raise HTTPException(status_code=401, detail="Missing X-User header")
return UserContext(username=x_user, role="operator")
@app.middleware("http")
async def add_process_time_header(request: Request, call_next):
started = time.perf_counter()
response = await call_next(request)
response.headers["X-Process-Time"] = f"{time.perf_counter() - started:.6f}"
return response
@app.get("/items/{sku}", response_model=ItemOut)
async def read_item(sku: str, user: UserContext = Depends(current_user)):
internal_record = {
"sku": sku.upper(),
"name": "Replacement Filter",
"cost_cents": 1299,
"visible_to": user.username,
}
return internal_record
Calling GET /items/rf-10 without X-User returns 401 and the endpoint does not execute. Calling it with X-User: dana returns SKU, name, and visible_to. The internal cost_cents field is omitted because it is not in ItemOut. The response also includes an X-Process-Time header added by middleware.
Worked Testing Example
FastAPI applications can be tested without opening a network port. TestClient drives the ASGI app in process and lets tests assert status codes, response bodies, and headers. This example defines the app and tests in one file so the behavior is explicit.
from fastapi import FastAPI
from fastapi.testclient import TestClient
from pydantic import BaseModel, Field
app = FastAPI()
class NoteIn(BaseModel):
text: str = Field(min_length=1)
@app.post("/notes", status_code=201)
def create_note(note: NoteIn):
return {"id": 1, "text": note.text.strip()}
client = TestClient(app)
def test_create_note_returns_created_note():
response = client.post("/notes", json={"text": " check seals "})
assert response.status_code == 201
assert response.json() == {"id": 1, "text": "check seals"}
def test_create_note_rejects_empty_text():
response = client.post("/notes", json={"text": ""})
assert response.status_code == 422
assert response.json()["detail"][0]["loc"][-1] == "text"
The first test proves the successful contract: status 201 and trimmed text. The second proves validation happens before the endpoint result is produced. The exact wording of validation messages can vary across Pydantic versions, so the test checks the stable facts: status code and field location.
Design Choices and Trade-offs
FastAPI encourages type-driven design, but annotations are not a replacement for careful contracts. A string annotation says a value is text; Field(min_length=3) says more about acceptable business input. Pydantic conversion is convenient, such as turning the query string ?notify=true into True, but strict APIs may need stricter field types to avoid accepting ambiguous client data.
Asynchronous endpoint functions are useful when the function awaits non-blocking I/O, such as an async database driver or HTTP client. A CPU-heavy calculation inside async def can block the event loop and hurt concurrent requests. Synchronous endpoints are run in a threadpool by Starlette, which is practical for blocking libraries, but threadpool capacity is still finite. Choose the endpoint style based on the libraries being called, not on fashion.
Response models add work because outgoing data is validated and serialized, but they reduce accidental data exposure and keep OpenAPI documentation accurate. Dependencies centralize repeated request preparation, but deeply nested dependencies can hide control flow. Middleware is powerful for behavior that truly wraps every request, but route-specific logic is usually clearer as dependencies or endpoint code.
Failure Modes and Troubleshooting
A common symptom is HTTP 422 when a client expected the endpoint to run. The cause is usually a mismatch between the function signature and the request. For example, a Pydantic model parameter expects a JSON body, while a scalar parameter without Path or Body may be interpreted as a query parameter. Diagnose this by reading the detail array in the response and checking each loc value. Correct the client payload or make the parameter source explicit.
Another symptom is HTTP 404 for a path that appears to exist. Causes include a missing router include, an unexpected path prefix, a trailing segment mismatch, or using the wrong application object when starting Uvicorn. Check the generated OpenAPI document at /openapi.json or the interactive docs route to confirm which paths were registered. Correct the router prefix or the uvicorn package.module:app target.
A third symptom is poor concurrency: one slow request makes unrelated requests wait. The cause may be blocking I/O or CPU work inside an async endpoint. Diagnose with timing logs, a small load test, and review of any calls made without await. Correct it by using async-compatible libraries, moving blocking work to a threadpool or task system, or keeping the endpoint synchronous when it calls blocking code.
A fourth symptom is private fields appearing in responses. The cause is usually returning raw dictionaries or ORM objects without a response model. Diagnose by comparing the endpoint’s return value with the declared response contract. Correct it by adding a response model and tests that assert sensitive fields are absent.
Security, Performance, and Reliability
FastAPI validates syntax and shape, but it does not decide your authorization policy. Dependencies are the usual place to authenticate a request and produce a user context. Endpoints should still enforce resource-specific authorization, such as whether that user can read a specific warehouse. Validation errors should be useful to clients without exposing secrets or internal stack traces.
Performance depends on the route work, serialization cost, dependency work, and the server configuration. Avoid reading large bodies unnecessarily, avoid expensive dependencies on routes that do not need them, and use response models deliberately when filtering matters. Reliability improves when startup checks verify essential configuration, tests exercise validation failures, and handlers return intentional HTTP errors instead of broad 500 responses.
Hands-on Lab
Prerequisites: Python installed, a virtual environment available, and the packages fastapi, uvicorn, and pytest installed in that environment. Create a new directory and add a file named main.py containing the stock example from this lesson. Start the app with uvicorn main:app --reload. In another terminal, send a valid POST request to http://127.0.0.1:8000/warehouses/7/stock?notify=true with JSON body {"sku":"abc123","quantity":4}. Verify that the response contains warehouse id 7, SKU ABC123, quantity 4, and notification requested true.
Next, send an invalid body with a two-character SKU and quantity zero. Verification succeeds when the response status is 422 and the error detail points to both invalid fields. Then open /docs in a browser and inspect how the path parameter, query parameter, and request body schema were generated from the Python function. Cleanup is simple: stop Uvicorn with Ctrl-C and remove the temporary lab directory. If you installed packages only for this lab, remove the virtual environment.
Assessment Exercises
- Given an endpoint parameter
limit: int = 10, explain where FastAPI reads the value from and how you would add a minimum and maximum. - A client sends valid JSON but receives 422. Describe a diagnostic process using the validation response and the endpoint signature.
- Refactor an endpoint that returns an internal dictionary with private fields so that only public fields are serialized.
- Choose between
defandasync deffor an endpoint that calls a blocking file-processing library, and justify the choice. - Design a dependency that reads a user header and explain which parts should return 401, which parts should return 403, and which parts belong in the endpoint.
Summary
FastAPI works by combining ASGI request handling, Starlette routing, Python signature inspection, Pydantic validation, dependency resolution, endpoint execution, and response serialization. The framework’s productivity comes from making the function signature carry much of the API contract. The trade-off is that you must understand how FastAPI interprets that signature. Once you can trace a request through those stages, later FastAPI topics become easier to reason about because they attach to known points in the same lifecycle.
