← Back to Basic Index

FastAPI - Basic: All Questions and Answers 15 topics · 45 Q&A

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 /docs and 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 def endpoints 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 def and FastAPI runs them in a thread pool, or with async def and 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 = module main.py, object app.
  • --reload restarts 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 --reload fail 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:app if the file is app/main.py).
Q: Should I use --reload in 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 → automatic 422 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 Entity with 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. Use Path() or Query() 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/on and false/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() or Query() 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] = None the same as str = None?
A: Practically both make the parameter optional, but Optional[str] = None (or str | None = None, Python 3.10+) is the correct typing; a plain str = None lies 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; EmailStr requires pip 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, or extra="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 / UserOut classes.
  • 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 passwd instead of password ships a bug silently. response_model enforces 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)

{ "detail": [ { "type": "greater_than", "loc": ["path", "item_id"], "msg": "Input should be greater than 0", "input": "-5" } ] }

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 RequestValidationError handler and return JSONResponse(status_code=400, ...) instead of 422, or pass exception_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 install fastapi[standard].
Q: How do I limit upload size?
A: Check file.size if available, or read in chunks with await file.read(1024 * 1024) counting bytes and abort when over the limit; alternatively enforce the limit at the proxy (Nginx client_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 UploadFile as a separate parameter next to your Form() 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_token matches X-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_cookie on a locally-created response instead of the injected one; CORS — the frontend must use credentials: "include" and the server must set Access-Control-Allow-Credentials: true with an exact origin (wildcard * blocks cookies); or secure=True while testing over plain HTTP.
Q: Headers vs cookies vs local storage for auth tokens?
A: Authorization headers 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"])
  • prefix can be set on the router or at include_router time (they concatenate).
  • Routers can be included multiple times with different prefixes (e.g. /api/v1 and /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 declare db: 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_db share one session per request. Disable with Depends(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}
The golden rule: never call blocking I/O inside 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

SituationUse
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 sureplain 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 4 uses 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 def make my endpoint run in parallel with others automatically?
A: It allows interleaved execution at every await point. 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=["*"] with allow_credentials=True — the spec forbids it for credentialed requests and it is insecure.
  • Preflight: non-simple requests trigger an OPTIONS preflight; 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/json is non-simple, so the browser sends a preflight OPTIONS. Either allow_headers does not include Content-Type/your custom header, or another handler intercepts OPTIONS before 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-generator create 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 in openapi_tags get 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 Jinja2Templates instance once (e.g. in a core/templates.py module or via a dependency) and import it in your routers — creating one per request/module wastes resources.