FastAPI - Basic: All Questions and Answers 15 topics · 45 Q&A
Contents — click a topic to jump to it (every section is collapsible):
01 Introduction to FastAPI
FastAPI is a modern, fast (high-performance) web framework for building APIs with Python 3.8+ based on standard Python type hints. It runs on ASGI servers such as Uvicorn, and is built on two libraries:
- Starlette: provides the asynchronous web foundation (routing, requests/responses, middleware, WebSockets).
- Pydantic: provides data validation and serialization using Python type hints.
Key Features
- Fast: one of the fastest Python frameworks, on par with Node.js and Go (thanks to Starlette and Uvicorn).
- Automatic validation: invalid data returns clear 422 errors automatically.
- Automatic interactive documentation: Swagger UI at
/docsand ReDoc at/redoc, generated from the OpenAPI schema (/openapi.json). - Type hints everywhere: full editor and mypy support, fewer runtime bugs.
- Dependency injection: built-in, declarative (
Depends) for sharing logic like DB sessions or authentication. - Async support: native
async defendpoints for I/O-bound workloads. - Standards-based: OpenAPI (formerly Swagger) and JSON Schema.
FastAPI vs Flask vs Django
- Flask: synchronous micro-framework; validation, serialization and docs must be added by hand. FastAPI gives all of that out of the box.
- Django / DRF: batteries included (ORM, admin, templates), heavier and traditionally sync-first. FastAPI is lighter and async-first, ideal for pure APIs and microservices.
- FastAPI: best when you want a typed, validated, auto-documented API with high concurrency.
Quick Questions and Answers
- Q: Is FastAPI only for asynchronous code?
- A: No. You can write endpoints with normal
defand FastAPI runs them in a thread pool, or withasync defand it runs them on the event loop. You can mix both in the same application. - Q: What does "ASGI" mean and why does it matter?
- A: ASGI is the async successor of WSGI. While WSGI (Flask/Django traditionally) handles one request per worker at a time, ASGI supports asynchronous, long-lived connections (WebSockets, SSE) and thousands of concurrent connections on a single process.
- Q: What is OpenAPI's role in FastAPI?
- A: FastAPI automatically generates an OpenAPI schema describing every path, parameter, request body and response. That schema powers the interactive docs and can be used to generate client code in other languages.
02 Installation and First App
Install in a virtual environment and run the development server:
python -m venv venv
venv\Scripts\activate # Windows
source venv/bin/activate # Linux / macOS
pip install "fastapi[standard]" # includes uvicorn, python-multipart, ...
# classic minimal alternative
pip install fastapi uvicorn
First Application: main.py
from fastapi import FastAPI
app = FastAPI()
@app.get("/")
def read_root():
return {"Hello": "World"}
@app.get("/items/{item_id}")
def read_item(item_id: int):
return {"item_id": item_id}
Running the Server
fastapi dev main.py # modern CLI (fastapi[standard])
uvicorn main:app --reload # classic way with uvicorn
main:app= modulemain.py, objectapp.--reloadrestarts on code changes (development only).- Default address:
http://127.0.0.1:8000. Docs:http://127.0.0.1:8000/docs.
Anatomy of the App
FastAPI(): the application instance; routes, middleware and events attach to it.- Path operation decorators (
@app.get,@app.post, ...): declare HTTP method and path. - Path operation function: runs per request; its return value is serialized to JSON by default.
- Type hints (
item_id: int) power validation and conversion: an invalid value returns 422 automatically.
Quick Questions and Answers
- Q: What is the difference between uvicorn and FastAPI?
- A: FastAPI is the framework (routing, validation, docs). Uvicorn is the ASGI server that accepts TCP connections and talks to the app. You need both: the server hosts the application.
- Q: Why does
uvicorn main:app --reloadfail with "ModuleNotFoundError"? - A: The command must run from the directory containing
main.py, or you must pass the full dotted path (e.g.app.main:appif the file isapp/main.py). - Q: Should I use
--reloadin production? - A: No. It watches files and restarts constantly, wasting resources. In production:
uvicorn main:app --host 0.0.0.0 --port 8000 --workers 4(or Gunicorn managing Uvicorn workers).
03 Path Parameters
Path parameters are variable parts of the URL path, declared with curly braces and received as typed function arguments:
from fastapi import FastAPI
app = FastAPI()
@app.get("/items/{item_id}")
def read_item(item_id: int):
return {"item_id": item_id}
GET /items/5→{"item_id": 5}(converted to int).GET /items/foo→ automatic422 Unprocessable Entity.
Order Matters: Fixed Paths Before Dynamic
@app.get("/users/me")
def read_current_user():
return {"user_id": "the current user"}
@app.get("/users/{user_id}") # must be declared AFTER /users/me
def read_user(user_id: str):
return {"user_id": user_id}
Enums as Constrained Values
from enum import Enum
class ModelName(str, Enum):
alexnet = "alexnet"
resnet = "resnet"
lenet = "lenet"
@app.get("/models/{model_name}")
def get_model(model_name: ModelName):
return {"model_name": model_name}
Only the listed values are accepted; anything else returns 422 with the valid options.
Extra Validation with Path() and Multi-Segment Paths
from fastapi import Path
@app.get("/items/{item_id}")
def read_item(item_id: int = Path(title="The ID of the item", ge=1, le=1000)):
return {"item_id": item_id}
@app.get("/files/{file_path:path}") # :path captures slashes too
def read_file(file_path: str):
return {"file_path": file_path}
Quick Questions and Answers
- Q: What error does FastAPI return for an invalid path parameter type?
- A:
422 Unprocessable Entitywith a JSON body listing the exact location, message and type of each validation error, e.g.{"loc": ["path", "item_id"], "msg": "value is not a valid integer"}. - Q: Can a path parameter have a default value?
- A: No. Path parameters are always required because they are part of the URL structure. Only query parameters can be optional.
- Q: How does FastAPI decide if a parameter is a path or query parameter?
- A: By name: if the parameter name appears in the route template (
{item_id}), it is a path parameter; otherwise it is read from the query string. UsePath()orQuery()to be explicit.
04 Query Parameters
Query parameters are the key-value pairs after the ? in a URL (/items?skip=0&limit=10). Any function parameter that is not in the path is a query parameter:
from typing import Optional
from fastapi import FastAPI, Query
app = FastAPI()
@app.get("/items/")
def read_items(skip: int = 0, limit: int = 10): # defaults
return {"skip": skip, "limit": limit}
@app.get("/search/")
def search(q: Optional[str] = None): # optional
return {"q": q}
@app.get("/tags/")
def filter_tags(tags: list[str] = []): # multiple values
return {"tags": tags}
@app.get("/products/")
def list_products(active: bool = True): # bool coercion
return {"active": active}
q: Optional[str] = None: optional; a parameter with no default (name: str) is required — missing it returns 422.- Booleans accept
true/1/yes/onandfalse/0/no/off. /tags/?tags=python&tags=api→["python", "api"].
Validation with Query()
@app.get("/items/")
def read_items(
q: str | None = Query(default=None, min_length=3, max_length=50, pattern="^fixedquery"),
page: int = Query(default=1, gt=0),
):
return {"q": q, "page": page}
Query() adds constraints (min_length, pattern, gt/ge) and metadata that appear both in validation errors and in the OpenAPI docs.
Quick Questions and Answers
- Q: How does FastAPI distinguish path parameters from query parameters?
- A: By name. If the parameter name appears in the path template it is a path parameter; otherwise it is read from the query string. Declare
Path()orQuery()to be explicit when a name could be both. - Q: What happens if a required query parameter is missing?
- A: The request fails with 422 and a message like
{"loc": ["query", "name"], "msg": "Field required"}— the client knows exactly what to fix. - Q: Is
Optional[str] = Nonethe same asstr = None? - A: Practically both make the parameter optional, but
Optional[str] = None(orstr | None = None, Python 3.10+) is the correct typing; a plainstr = Nonelies to the type checker and modern FastAPI versions may reject it.
05 Request Body and Pydantic Models
Structured JSON bodies are received by declaring a parameter with a Pydantic model type. FastAPI reads, validates and converts the body into a typed object:
from pydantic import BaseModel, Field, EmailStr
class Item(BaseModel):
name: str = Field(min_length=1, max_length=50, examples=["Keyboard"])
price: float = Field(gt=0, description="Price in USD")
is_offer: bool | None = None
owner_email: EmailStr | None = None
@app.post("/items/")
def create_item(item: Item):
return {"item_name": item.name, "price_with_tax": item.price * 1.12}
- Fields with a type and no default are required; with a default they are optional.
Field()adds constraints and docs metadata;EmailStrrequirespip install "pydantic[email]".
Combining Body, Path and Query Parameters
@app.put("/items/{item_id}")
def update_item(item_id: int, item: Item, q: str | None = None):
return {"item_id": item_id, **item.model_dump(), "q": q}
FastAPI routes each parameter to its source: item_id from the path, item from the JSON body, q from the query string.
Nested Models and Multiple Body Parameters
class Tag(BaseModel):
name: str
class Article(BaseModel):
title: str
tags: list[Tag] = [] # arbitrarily deep nesting works
@app.put("/items/{item_id}")
def update(item_id: int, item: Item, user: User):
# two models => body must be {"item": {...}, "user": {...}}
return {"item_id": item_id}
Force embedding of a single model with item: Item = Body(embed=True).
Quick Questions and Answers
- Q: What is the difference between .model_dump() and .model_dump_json()?
- A:
model_dump()returns a Python dict;model_dump_json()returns a JSON string directly (faster, no intermediate dict). In Pydantic v1 these were called.dict()and.json(). - Q: Does FastAPI validate the response body too?
- A: Only if you declare a
response_model. Without it, whatever the function returns is serialized as-is. With it, the output is filtered and validated against the model. - Q: What happens with extra/unknown fields in the request body?
- A: By default they are ignored. Change with
model_config = ConfigDict(extra="forbid")to reject unknown fields with a 422 error, orextra="allow"to keep them.
06 Response Models and Status Codes
response_model declares what an endpoint returns: FastAPI filters, validates and documents the output — the standard way to keep sensitive fields out of responses:
from pydantic import BaseModel, EmailStr
class UserIn(BaseModel):
username: str
password: str
email: EmailStr
class UserOut(BaseModel):
username: str
email: EmailStr
@app.post("/users/", response_model=UserOut)
def create_user(user: UserIn):
return user # password is processed but NEVER returned
Common Patterns
- Input vs output models: separate
UserIn/UserOutclasses. - Lists:
response_model=list[Item]; unions:response_model=Item | None. - Field control:
response_model_exclude_unset=True,response_model_include={"id", "name"},response_model_exclude={"secret_key"}.
Status Codes
from fastapi import status, Response
@app.post("/items/", status_code=status.HTTP_201_CREATED)
def create_item(item: Item):
return item
@app.delete("/items/{item_id}", status_code=status.HTTP_204_NO_CONTENT)
def delete_item(item_id: int):
return None # 204 must return no body
@app.post("/jobs/")
def create_job(response: Response): # runtime-decided status
response.status_code = status.HTTP_202_ACCEPTED
return {"queued": True}
Quick Questions and Answers
- Q: What happens if the returned object does not match response_model?
- A: FastAPI tries to coerce/validate it; if it truly does not fit, the server raises an Internal Server Error (500) and logs the validation error. This catches bugs before bad data reaches clients.
- Q: Does response_model cost performance?
- A: There is an extra validation pass on the way out, but Pydantic v2 is Rust-based and fast; it buys safety and clean docs. For very hot endpoints you can skip it, but you lose the output contract.
- Q: Why not just return a dict with the right keys?
- A: Dicts are untyped: one developer returning
passwdinstead ofpasswordships a bug silently.response_modelenforces the shape and documents it automatically.
07 Validation and Error Handling
Three error layers: automatic 422 validation errors, HTTPException for expected application errors, and custom exception handlers for shaping responses.
HTTPException: The Standard Way
from fastapi import HTTPException
@app.get("/items/{item_id}")
def read_item(item_id: int):
if item_id not in items_db:
raise HTTPException(
status_code=404,
detail="Item not found",
headers={"WWW-Authenticate": "Bearer"}, # optional headers
)
return items_db[item_id]
Produces {"detail": "Item not found"} with status 404. detail can be any JSON-serializable value.
Automatic Validation Errors (422)
loc tells you where the problem is: path, query, body (nested as body.field.subfield).
Custom Exception Handlers
from fastapi import Request
from fastapi.responses import JSONResponse
from fastapi.exceptions import RequestValidationError
@app.exception_handler(RequestValidationError)
async def validation_handler(request: Request, exc: RequestValidationError):
return JSONResponse(status_code=422, content={"errors": exc.errors()})
class UnicornException(Exception):
pass
@app.exception_handler(UnicornException)
async def unicorn_handler(request: Request, exc: UnicornException):
return JSONResponse(status_code=418, content={"message": f"Oops: {exc}"})
Pydantic Validators for Business Rules
from pydantic import field_validator
class Order(BaseModel):
quantity: int
@field_validator("quantity")
@classmethod
def quantity_positive(cls, v: int) -> int:
if v <= 0:
raise ValueError("quantity must be positive")
return v
Model validators for data-shaped rules (formats, ranges); HTTPException inside endpoints for resource-state rules (does it exist?, is the user allowed?).
Quick Questions and Answers
- Q: When should I use HTTPException vs returning an error dict?
- A: Always raise. Returning
{"error": ...}with status 200 breaks clients that check the status code and pollutes the docs. Exceptions also stop execution immediately. - Q: How do I return a different status code for validation errors?
- A: Override the
RequestValidationErrorhandler and returnJSONResponse(status_code=400, ...)instead of 422, or passexception_handlers={...}at app creation. - Q: Where do unhandled exceptions go?
- A: They bubble up to the server, which returns a generic 500 and logs the traceback. Catch what you can meaningfully handle; add monitoring (Sentry, OpenTelemetry) in production.
08 Form Data and File Uploads
HTML forms send application/x-www-form-urlencoded or multipart/form-data (with files). Requires pip install python-multipart.
from fastapi import Form, UploadFile
from typing import List
@app.post("/login/")
def login(username: str = Form(), password: str = Form()):
return {"username": username}
@app.post("/uploadfile/")
async def create_upload_file(file: UploadFile):
contents = await file.read()
return {"filename": file.filename, "content_type": file.content_type, "size": len(contents)}
@app.post("/upload-multiple/")
async def upload_multiple(files: List[UploadFile]):
return {"names": [f.filename for f in files]}
UploadFile vs bytes
file: bytes = File(): loads the whole file into memory — fine for small files, dangerous for large ones.file: UploadFile: streamed; has metadata (filename,content_type) and async methods (read(size),seek(0),close()). Preferred.
Saving an Upload Safely
import shutil, uuid
from pathlib import Path
UPLOAD_DIR = Path("uploads")
UPLOAD_DIR.mkdir(exist_ok=True)
@app.post("/save-file/")
async def save_file(file: UploadFile):
safe_name = f"{uuid.uuid4()}{Path(file.filename).suffix}" # never trust client filename
dest = UPLOAD_DIR / safe_name
with dest.open("wb") as out:
shutil.copyfileobj(file.file, out)
return {"saved_as": safe_name}
Security: never use the client-supplied filename directly (path traversal), validate content_type and size limits.
Quick Questions and Answers
- Q: Why does the endpoint stop working with "Form data requires python-multipart"?
- A: Form and file parsing are not part of FastAPI's core; install the parser with
pip install python-multipart. It is included when you installfastapi[standard]. - Q: How do I limit upload size?
- A: Check
file.sizeif available, or read in chunks withawait file.read(1024 * 1024)counting bytes and abort when over the limit; alternatively enforce the limit at the proxy (Nginxclient_max_body_size). - Q: Can UploadFile be used with a Pydantic model?
- A: Not directly inside a JSON model — files are multipart, not JSON. Declare the
UploadFileas a separate parameter next to yourForm()fields.
09 Headers and Cookies
Read headers/cookies with Header and Cookie; write them through the injected Response:
from fastapi import Header, Cookie, Response
from typing import Optional
@app.get("/items/")
def read_items(
user_agent: Optional[str] = Header(default=None), # User-Agent header
x_token: Optional[str] = Header(default=None), # X-Token header
):
return {"User-Agent": user_agent, "X-Token": x_token}
@app.get("/preferences/")
def get_preferences(session_id: Optional[str] = Cookie(default=None)):
return {"session_id": session_id}
Underscores convert to hyphens automatically (user_agent reads User-Agent); header lookup is case-insensitive.
Setting Headers and Cookies
@app.post("/set-cookie/")
def set_cookie(response: Response):
response.headers["X-Process-Time"] = "0.42"
response.set_cookie(
key="session_id", value="abc123",
httponly=True, # JavaScript cannot read it (XSS protection)
secure=True, # only sent over HTTPS
samesite="lax", # CSRF mitigation
max_age=3600,
)
return {"message": "cookie set"}
@app.delete("/unset-cookie/")
def unset_cookie(response: Response):
response.delete_cookie("session_id")
return {"message": "cookie removed"}
Headers You Will Actually Use
- Authorization: tokens (JWT) — usually read in a dependency.
- X-Request-ID / traceparent: correlation IDs for distributed tracing.
- Accept / Accept-Language: content negotiation.
- Cache-Control, ETag: HTTP caching.
Quick Questions and Answers
- Q: Header names are case-insensitive in HTTP — does FastAPI care?
- A: No. Declaring
x_tokenmatchesX-Token,x-token, etc. The underscore-to-hyphen conversion is the only transformation applied. - Q: Why is my Set-Cookie header not reaching the browser?
- A: Common causes: calling
set_cookieon a locally-created response instead of the injected one; CORS — the frontend must usecredentials: "include"and the server must setAccess-Control-Allow-Credentials: truewith an exact origin (wildcard*blocks cookies); orsecure=Truewhile testing over plain HTTP. - Q: Headers vs cookies vs local storage for auth tokens?
- A:
Authorizationheaders are the standard for APIs (stateless, no CSRF cookie issues). HttpOnly cookies protect against XSS but need CSRF protection. localStorage is readable by any injected script — generally avoid for tokens.
10 API Routers and Project Structure
APIRouter splits path operations into modules and mounts them on the app with prefixes and tags:
# routers/users.py
from fastapi import APIRouter
router = APIRouter(prefix="/users", tags=["users"])
@router.get("/")
def list_users():
return [{"username": "alice"}]
# main.py
from fastapi import FastAPI
from routers import users, items
app = FastAPI()
app.include_router(users.router)
app.include_router(items.router, prefix="/api", tags=["items"])
prefixcan be set on the router or atinclude_routertime (they concatenate).- Routers can be included multiple times with different prefixes (e.g.
/api/v1and/api/v2).
A Scalable Layout (Bigger Apps)
app/
├── main.py # app factory, middleware, include routers
├── core/config.py # pydantic-settings
├── core/security.py # hashing, JWT
├── db/session.py # engine, SessionLocal
├── models/ # SQLAlchemy / DB models
├── schemas/ # Pydantic models (in/out)
├── crud/ # DB operations per entity
├── routers/ # APIRouter per entity
├── dependencies.py # shared Depends (get_db, current_user)
└── tests/
Layering: routers depend on crud and schemas; crud depends on models. This keeps layers swappable and testable.
Quick Questions and Answers
- Q: Can two routers define the same path?
- A: Yes, and the first matching route registered wins. This is a common source of "my endpoint returns the wrong thing" bugs — check include order, especially for catch-alls like
/{item_id}declared before fixed paths. - Q: Should I create the FastAPI() app inside main.py or in a factory function?
- A: For anything beyond a demo, use a factory:
def create_app() -> FastAPI:that wires routers, middleware and events. It makes testing (fresh app per suite) and per-environment configs trivial. - Q: Where do shared dependencies like get_db live?
- A: In a central
dependencies.py(or per-domain module). Routers import them and declaredb: Session = Depends(get_db)— never create sessions directly inside endpoints.
11 Dependency Injection Basics (Depends)
Depends declares things your endpoint needs (DB sessions, auth, pagination) as parameters. FastAPI builds them per-request and caches them within that request:
from fastapi import Depends, HTTPException, Header
async def pagination(skip: int = 0, limit: int = 20):
return {"skip": skip, "limit": limit}
def verify_token(x_token: str = Header()):
if x_token != "secret":
raise HTTPException(status_code=401, detail="Invalid token")
return x_token
@app.get("/items/")
def list_items(page: dict = Depends(pagination)):
return {"page": page}
@app.get("/admin/")
def admin_panel(token: str = Depends(verify_token)):
return {"access": "granted"}
Raising HTTPException inside a dependency aborts the request before the endpoint runs — the natural place for auth, authorization and rate limiting.
Resources with Cleanup: yield Dependencies
def get_db():
db = SessionLocal()
try:
yield db # endpoint runs with this value
finally:
db.close() # always runs afterwards (like a context manager)
Dependency Trees and Caching
def get_current_user(token: str = Depends(verify_token), db=Depends(get_db)):
...
@app.get("/me/")
def me(user=Depends(get_current_user)):
...
- Dependencies can depend on other dependencies — a tree resolved per request.
- Same-request caching: two dependencies using
get_dbshare one session per request. Disable withDepends(fn, use_cache=False).
Class Dependencies and Shortcuts
class CommonParams:
def __init__(self, q: str | None = None, skip: int = 0, limit: int = 20):
self.q, self.skip, self.limit = q, skip, limit
@app.get("/things/")
def list_things(common: CommonParams = Depends()): # Depends() uses the class itself
return {"q": common.q}
Quick Questions and Answers
- Q: When does a dependency run — and how often?
- A: Once per request, before the endpoint, resolved lazily as the dependency tree is built. Results are cached for that single request only — they are not singletons; use module-level objects (engine, connection pools) for cross-request state.
- Q: Can I apply a dependency to a whole router without using its value?
- A: Yes:
router = APIRouter(dependencies=[Depends(verify_token)])runs the check on every route but discards the result. Also available per-endpoint:@app.get("/", dependencies=[...]). - Q: Dependencies vs middleware — which one do I use?
- A: Middleware runs for every request globally with no access to route parameters — CORS, timing, logging. Dependencies are per-route, typed, can raise HTTP errors and receive validated parameters — auth, pagination, DB sessions.
12 async / await in FastAPI
The single most important performance decision: choosing between def and async def correctly.
import httpx
@app.get("/sync-mode/")
def sync_mode():
result = blocking_call() # runs in a worker thread (default 40 threads)
@app.get("/async-mode/")
async def async_mode():
async with httpx.AsyncClient() as client: # runs on the event loop
result = await client.get("https://api.example.com/data")
return {"ok": True}
async def. time.sleep(5) or a synchronous DB query inside an async def freezes the entire event loop — the whole server stops serving all requests. Use plain def (threadpool), or wrap it: await asyncio.to_thread(blocking_fn).
Decision Guide
| Situation | Use |
|---|---|
| Async libraries (httpx, asyncpg, motor, aiofiles) | async def |
| Blocking libraries (requests, psycopg2, pandas, boto3 sync) | plain def |
| Pure CPU work (no I/O) | plain def; offload heavy compute to Celery/RQ or another process |
| Not sure | plain def — always safe (threadpool), just less scalable |
Concurrency vs Parallelism
- Async gives concurrency: while one request waits on I/O, the event loop serves others. It does not speed up CPU work.
- Workers give parallelism:
uvicorn --workers 4uses multiple cores (rule of thumb:2 × cores + 1). Usually you want both.
Calling Multiple Things in Parallel
import asyncio
@app.get("/dashboard/")
async def dashboard():
async with httpx.AsyncClient() as client:
user, orders = await asyncio.gather(
client.get("https://api.example.com/user/1"),
client.get("https://api.example.com/orders?user=1"),
)
return {"user": user.json(), "orders": orders.json()} # latency = slower call, not the sum
Quick Questions and Answers
- Q: My async endpoint is slower than the sync version. Why?
- A: Almost always because it does blocking work on the event loop (sync DB driver, requests, CPU-heavy pandas). Everything queues behind those calls. Switch to async libraries or declare the endpoint as plain
def. - Q: Does
async defmake my endpoint run in parallel with others automatically? - A: It allows interleaved execution at every
awaitpoint. Two async endpoints progress concurrently while they await I/O, but the code between awaits still runs one at a time on the single loop thread. - Q: What is the default threadpool size for sync endpoints?
- A: AnyIO's default is 40 threads. Tune it at startup:
anyio.to_thread.current_default_thread_limiter().total_tokens = 100. Beyond the limit, sync requests queue up.
13 Middleware and CORS
Middleware wraps every request/response — the place for CORS, logging, timing, compression and request IDs. Code before call_next runs on the way in; after, on the way out:
import time
from fastapi import Request
@app.middleware("http")
async def add_process_time_header(request: Request, call_next):
start = time.perf_counter()
response = await call_next(request)
response.headers["X-Process-Time"] = f"{time.perf_counter() - start:.4f}"
return response
CORS: The Most Common Middleware
from fastapi.middleware.cors import CORSMiddleware
app.add_middleware(
CORSMiddleware,
allow_origins=["https://mysite.com", "http://localhost:3000"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
- Never combine
allow_origins=["*"]withallow_credentials=True— the spec forbids it for credentialed requests and it is insecure. - Preflight: non-simple requests trigger an
OPTIONSpreflight; CORSMiddleware answers it automatically. - CORS is enforced by the browser; curl/Postman ignore it. "Works in Postman but not in the browser" is almost always CORS.
Built-in and Class-Based Middleware
from fastapi.middleware.gzip import GZipMiddleware
app.add_middleware(GZipMiddleware, minimum_size=1000) # compress big responses
class RequestIDMiddleware(BaseHTTPMiddleware):
async def dispatch(self, request, call_next):
request.state.request_id = str(uuid.uuid4())
response = await call_next(request)
response.headers["X-Request-ID"] = request.state.request_id
return response
app.add_middleware(RequestIDMiddleware)
- Also built-in: TrustedHostMiddleware (host-header defense), HTTPSRedirectMiddleware.
- Order matters: the last middleware added is the outermost — it runs first on requests and last on responses.
Quick Questions and Answers
- Q: Middleware vs dependencies — when to use which?
- A: Middleware sees every request at the raw ASGI level (no validated parameters) — CORS, compression, timing, tracing. Dependencies are per-route, typed, can raise HTTP errors and appear in docs — auth, DB sessions, pagination.
- Q: Does middleware execution order matter?
- A: Yes. The last middleware added wraps everything else. Plan order accordingly (e.g. request-ID middleware should wrap all others).
- Q: My CORS setup works for GET but fails for POST with JSON. Why?
- A: A POST with
Content-Type: application/jsonis non-simple, so the browser sends a preflightOPTIONS. Eitherallow_headersdoes not includeContent-Type/your custom header, or another handler interceptsOPTIONSbefore the CORS middleware.
14 OpenAPI Docs and Metadata
FastAPI generates an OpenAPI schema automatically. Three URLs come for free:
/openapi.json— the raw OpenAPI 3 schema./docs— Swagger UI: try endpoints from the browser ("Try it out")./redoc— ReDoc: cleaner read-only reference.
Enriching the Schema
app = FastAPI(
title="Inventory API",
description="API for managing warehouse inventory.",
version="1.0.0",
openapi_tags=[
{"name": "items", "description": "CRUD for items"},
{"name": "health", "description": "Service status"},
],
)
@app.post("/items/", tags=["items"], summary="Create an item",
response_description="The created item", status_code=201)
def create_item(item: Item):
return {"id": 1, **item.model_dump()}
Every parameter accepts title, description, examples; models get Field(description=...); decorators get summary, description, tags.
Documenting Responses
class ErrorMessage(BaseModel):
detail: str
@app.get("/items/{item_id}",
responses={
404: {"model": ErrorMessage, "description": "Item not found"},
400: {"description": "Invalid id format"},
})
def read_item(item_id: int): ...
Hiding and Customizing
app = FastAPI(docs_url="/documentation", redoc_url=None, openapi_url="/schema.json")
@app.get("/internal/", include_in_schema=False) # keep out of the docs
def internal(): ...
Using the Schema Beyond Docs
- Client generation:
openapi-typescript,openapi-generatorcreate typed clients from/openapi.json. - Contract testing: schemathesis fuzzes your API using the schema.
- API gateways: import the schema (Kong, Azure APIM, AWS API Gateway).
Quick Questions and Answers
- Q: Where do the docs come from — do I write OpenAPI YAML by hand?
- A: Never by hand. FastAPI introspects your paths, parameters, Pydantic models and declared status codes, then builds the schema at startup. Your Python code is the single source of truth.
- Q: How do I group endpoints by resource in the UI?
- A: Use
tags=["items"]on routers or endpoints. Tags declared inopenapi_tagsget descriptions and a fixed order in the docs. - Q: Can I test endpoints from the docs UI against my real database?
- A: Yes — the docs call the real server. In shared environments protect or disable docs (
docs_url=None, or gate them behind auth) since "Try it out" executes real mutations.
15 Static Files and Templates
FastAPI can also serve static assets and render server-side HTML with Jinja2 — useful for admin pages, dashboards and small internal tools.
Serving Static Files
from fastapi.staticfiles import StaticFiles
app.mount("/static", StaticFiles(directory="static"), name="static")
# StaticFiles(directory="static", html=True) also serves index.html
app.mount()attaches a full ASGI sub-application at a path prefix.- In production, serve assets with Nginx/CDN if performance matters.
Jinja2 Templates
pip install jinja2
from fastapi.responses import HTMLResponse
from fastapi.templating import Jinja2Templates
templates = Jinja2Templates(directory="templates")
@app.get("/dashboard/", response_class=HTMLResponse)
async def dashboard(request: Request):
stats = {"total_items": 42, "low_stock": 3}
return templates.TemplateResponse(
request=request, name="dashboard.html",
context={"title": "Dashboard", "stats": stats},
)
<h1>{{ title }}</h1>
<p>Total items: {{ stats.total_items }}</p>
{% if stats.low_stock > 0 %}
<p class="alert">{{ stats.low_stock }} items low on stock!</p>
{% endif %}
Jinja2 syntax: {{ variable }} for output, {% ... %} for logic, filters like {{ price | round(2) }}.
Returning Raw HTML
@app.get("/hello/", response_class=HTMLResponse)
def hello():
return "<h1>Hello</h1>"
Quick Questions and Answers
- Q: Should a FastAPI app render HTML, or should I split frontend and backend?
- A: For real products, prefer a separated SPA (React/Angular/Vue) consuming the API — independent deploys and scaling. Templates are great for server-rendered admin panels, emails, PDF reports or small internal tools.
- Q: Why does
app.mount("/static", ...)fail with a runtime error? - A: Usually because the directory does not exist relative to the working directory where Uvicorn was started. Create the folder or resolve the path from the file location:
Path(__file__).parent / "static". - Q: How do I use template responses inside routers without global state?
- A: Create the
Jinja2Templatesinstance once (e.g. in acore/templates.pymodule or via a dependency) and import it in your routers — creating one per request/module wastes resources.