Cheat SheetsFastAPIRequests & Validation

Requests & Validation — Cheat Sheet

FastAPI · 4 topics. Download the PDF or the Instagram carousel and share it.

Cheat Sheet · AiCanCode.org
Requests & Validation
FastAPI4 topicsQuick revision reference
1

Validation Deep Dive — Field, field_validator & model_validator

Field() handles declarative constraints (bounds, lengths, patterns); @field_validator runs custom logic per field; @model_validator checks rules across fields — all producing structured 422s.

  • Escalate: Field() constraints → field_validator → model_validator
  • Validators return the stored value — normalise (strip, title, +91) at the boundary
  • model_validator(mode="after") is the home for cross-field rules
  • Raise ValueError with a clear message; keep validators free of I/O and DB calls
Declarative first; validators for logic — and they normalise too
from pydantic import BaseModel, Field, field_validator

class StudentIn(BaseModel):
    name: str = Field(min_length=2, max_length=60)
    phone: str                                    # custom rule below
    cgpa: float = Field(ge=0, le=10)
    pincode: str = Field(pattern=r"^[1-9][0-9]{5}$")   # Indian pincode shape

    @field_validator("phone")
    @classmethod
    def valid_indian_mobile(cls, v: str) -> str:
        digits = "".join(ch for ch in v if ch.isdigit())
        if digits.startswith("91") and len(digits) == 12:
            digits = digits[2:]                   # strip +91 → normalise
        if len(digits) != 10 or digits[0] not in "6789":
            raise ValueError("must be a valid 10-digit Indian mobile")
        return digits                             # ← transformed value is stored

    @field_validator("name")
    @classmethod
    def tidy_name(cls, v: str) -> str:
        return v.strip().title()                  # "  asha rao " → "Asha Rao"

# StudentIn(name="asha rao", phone="+91 98765 43210", cgpa=8.7, pincode="413001")
#   → phone stored as "9876543210", name as "Asha Rao"
# phone="12345" → 422: {"loc": ["body", "phone"],
#                       "msg": "Value error, must be a valid 10-digit Indian mobile"}
2

Nested Models — Real-World Payloads

Models compose: a field typed as another BaseModel nests an object, list[Model] nests an array, and validation runs recursively with error paths like items.1.qty — plus rich types (EmailStr, UUID, datetime) and Enums for free.

  • Nest by typing: field: Address, items: list[OrderItem], recursive validation free
  • 422 loc paths walk the tree with list indexes — clients can pinpoint bad rows
  • EmailStr/HttpUrl/UUID/datetime/Decimal replace hand-rolled format regexes
  • str-Enum fields lock vocabulary and render as dropdowns in /docs
Recursive validation with tree-accurate error locations
from fastapi import FastAPI
from pydantic import BaseModel, Field

app = FastAPI()

class Address(BaseModel):
    line1: str
    city: str
    pincode: str = Field(pattern=r"^[1-9][0-9]{5}$")

class OrderItem(BaseModel):
    sku: str
    name: str
    qty: int = Field(ge=1, le=20)
    price: float = Field(gt=0)

class OrderIn(BaseModel):
    customer: str
    address: Address                       # nested object
    items: list[OrderItem] = Field(min_length=1)    # non-empty array
    coupon: str | None = None

@app.post("/orders")
def place_order(order: OrderIn):
    total = sum(i.qty * i.price for i in order.items)
    return {"city": order.address.city, "items": len(order.items), "total": total}

# Payload:
# {"customer": "Ravi",
#  "address": {"line1": "12 FC Road", "city": "Pune", "pincode": "411005"},
#  "items": [{"sku": "S1", "name": "Kurta", "qty": 2, "price": 799},
#            {"sku": "S2", "name": "Mojari", "qty": 0, "price": 1299}]}
#
# → 422: {"loc": ["body", "items", 1, "qty"],
#         "msg": "Input should be greater than or equal to 1"}
#   ← the SECOND item's qty, pinpointed. Frontend can highlight that row.
3

Forms & File Uploads — multipart/form-data

Form() reads HTML-form fields, File()/UploadFile handle uploads — UploadFile streams to a spooled temp file so big files don't eat RAM. Validate content type and size yourself; the framework won't.

  • python-multipart required; Form() = form fields, File()/UploadFile = uploads
  • UploadFile spools to disk with async read/seek — prefer it over bytes
  • filename and content_type come from the client — verify magic bytes yourself
  • Cap size while streaming; store under generated names, never client names
Form() for fields, UploadFile for files — same endpoint, one request
from fastapi import FastAPI, File, Form, UploadFile

app = FastAPI()
# pip install python-multipart   ← required for forms & uploads

@app.post("/login")                        # classic form post (no file)
def login(username: str = Form(), password: str = Form()):
    return {"user": username}              # OAuth2 password flow uses exactly this

@app.post("/marksheets")                   # fields + file in ONE request
async def upload_marksheet(
    student_id: int = Form(),
    semester: int = Form(ge=1, le=8),
    remarks: str = Form(default=""),
    sheet: UploadFile = File(),
):
    header = await sheet.read(5)           # first bytes (magic number)
    await sheet.seek(0)                    # rewind before the real read
    content = await sheet.read()

    return {
        "student": student_id,
        "semester": semester,
        "filename": sheet.filename,        # client-supplied — do NOT trust
        "declared_type": sheet.content_type,   # also client-supplied
        "size_bytes": len(content),
        "is_pdf": header == b"%PDF-",      # verified from actual bytes
    }

# bytes vs UploadFile:
#   photo: bytes = File()     → whole file in RAM (fine for tiny files only)
#   photo: UploadFile         → spooled temp file, async streaming — DEFAULT
# Multiple files: photos: list[UploadFile] = File()
4

Headers, Cookies & the Request Object

Header() and Cookie() bind request metadata as typed parameters (X-API-Key becomes x_api_key); Response.set_cookie sends session cookies with the security flags that interviews love; Request is the escape hatch.

  • Header()/Cookie() bind metadata as typed params; x_api_key ↔ X-Api-Key mapping is automatic
  • set_cookie with httponly + secure + samesite="lax" is the baseline session recipe
  • Request is the escape hatch: url, method, headers, client
  • Behind a proxy, the client IP is in X-Forwarded-For — and only trustworthy from your own proxy
Typed, documented access to metadata — no request.headers digging
from fastapi import FastAPI, Header, Cookie, HTTPException

app = FastAPI()

@app.get("/profile")
def profile(
    x_api_key: str = Header(),                     # required: X-Api-Key
    user_agent: str | None = Header(default=None), # standard header, optional
    x_request_id: str | None = Header(default=None),   # tracing id from gateway
    session_id: str | None = Cookie(default=None),     # cookie by name
):
    if x_api_key != "tophub-secret-1":             # (real auth → Auth chapters)
        raise HTTPException(401, "invalid API key")
    return {
        "client": user_agent,
        "trace": x_request_id,
        "has_session": session_id is not None,
    }

# curl http://127.0.0.1:8000/profile \
#      -H "X-Api-Key: tophub-secret-1" \
#      -H "X-Request-ID: req-7f3a" \
#      --cookie "session_id=abc123"

# Notes:
#   - underscore ↔ hyphen conversion is automatic (convert_underscores=True)
#   - headers are case-insensitive per HTTP spec
#   - Authorization: Bearer <token> is usually read via Security utilities
#     (OAuth2PasswordBearer) rather than raw Header() — coming in Auth
Learn this free with Aria, your AI tutor → AiCanCode.org/learn/fastapi