Especialização em Backend com Python & FastAPI • FastAPI & Python 3.12+ • Pydantic v2, SQLModel, Async/Await e Microsserviços


🗺️ Mapa Conceitual do Tópico

flowchart TD
    A["Requisição HTTP"] --> B{"Onde está o dado?"}
    B -->|"na URL fixa"| C["Path (obrigatório)"]
    B -->|"filtro opcional na URL"| D["Query"]
    B -->|"metadado de protocolo"| E["Header / Cookie"]
    B -->|"payload estruturado"| F["Body (Pydantic Model)"]
    B -->|"arquivo binário"| G["UploadFile (multipart)"]
    C & D & E & F & G --> H["FastAPI infere a origem\npela assinatura da função"]

    style A fill:#e1f5fe,stroke:#03a9f4,stroke-width:2px
    style B fill:#fff3e0,stroke:#ff9800,stroke-width:2px
    style C fill:#ede7f6,stroke:#7e57c2,stroke-width:2px
    style D fill:#e8f5e9,stroke:#4caf50,stroke-width:2px
    style E fill:#fce4ec,stroke:#e91e63,stroke-width:2px
    style F fill:#f3e5f5,stroke:#9c27b0,stroke-width:2px
    style G fill:#fffde7,stroke:#fbc02d,stroke-width:2px

🏛️ 1. Como o FastAPI decide de onde vem cada parâmetro

FastAPI não exige decoradores separados para cada tipo de entrada: ele inspeciona a assinatura da função e as anotações de tipo para decidir a origem de cada parâmetro. Um int ou str que aparece na string da rota ({item_id}) é tratado como path parameter automaticamente; um parâmetro com o mesmo nome que não aparece na rota vira query parameter por padrão; um BaseModel do Pydantic é interpretado como body. Essa inferência implícita é conveniente, mas para exercer controle explícito (validações extras, valores padrão, descrição no OpenAPI) usam-se os marcadores Path, Query, Header, Cookie e Body.

from typing import Optional
from fastapi import FastAPI, Path, Query

app = FastAPI()

@app.get("/items/{item_id}")
async def read_item(
    item_id: int = Path(..., ge=1, description="ID do item, sempre positivo"),
    q: Optional[str] = Query(None, min_length=3, max_length=50),
):
    return {"item_id": item_id, "filtro": q}

Body com múltiplos modelos no mesmo payload

Quando mais de um parâmetro é anotado como BaseModel (explícita ou implicitamente), o FastAPI aninha automaticamente cada um sob uma chave com seu próprio nome no JSON esperado — não é preciso criar um modelo “wrapper” manualmente.

from fastapi import FastAPI, Body, status
from pydantic import BaseModel

class ClienteRef(BaseModel):
    cliente_id: int
    nome: str

class ItemPedido(BaseModel):
    produto_id: int
    quantidade: int
    preco_unitario: float

app = FastAPI()

@app.post("/pedidos", status_code=status.HTTP_201_CREATED)
async def criar_pedido(cliente: ClienteRef = Body(...), itens: list[ItemPedido] = Body(...)):
    # Body esperado: {"cliente": {...}, "itens": [{...}, ...]}
    total = sum(item.quantidade * item.preco_unitario for item in itens)
    return {"cliente": cliente.nome, "total": total, "qtd_itens": len(itens)}

Headers e cookies: nomes com hífen viram snake_case

HTTP headers tradicionalmente usam kebab-case (X-Tenant-ID), o que não é um identificador Python válido. O FastAPI faz a conversão automática: um parâmetro x_tenant_id é mapeado para o header X-Tenant-ID na requisição. Cookie(...) segue a mesma lógica de declaração de Query/Header, incluindo suporte a valor padrão e validação de formato.

from typing import Optional
from fastapi import FastAPI, Header, Cookie, HTTPException

app = FastAPI()

@app.get("/tenant-info")
async def obter_tenant(
    x_tenant_id: str = Header(..., description="ID da organização"),
    session_id: Optional[str] = Cookie(None),
):
    if not x_tenant_id.startswith("corp_"):
        raise HTTPException(status_code=400, detail="Tenant ID inválido")
    return {"tenant": x_tenant_id, "session_active": session_id is not None}

Upload de arquivos com UploadFile

UploadFile (diferente de bytes puro) faz streaming do conteúdo em disco/memória sem carregar o arquivo inteiro de uma vez, o que é essencial para uploads grandes. Ele expõe .filename, .content_type e um objeto assíncrono .read(). Validar content_type e tamanho manualmente é necessário porque o cliente pode mentir sobre o Content-Type — a validação real de conteúdo (magic bytes) fica a cargo de bibliotecas especializadas em cenários de maior risco.

from fastapi import FastAPI, UploadFile, File, HTTPException

app = FastAPI()
MAX_FILE_SIZE = 5 * 1024 * 1024  # 5 MB

@app.post("/upload-documento")
async def upload_documento(arquivo: UploadFile = File(...)):
    if arquivo.content_type not in {"application/pdf", "image/png"}:
        raise HTTPException(status_code=415, detail="Tipo de mídia não suportado")
    conteudo = await arquivo.read()
    if len(conteudo) > MAX_FILE_SIZE:
        raise HTTPException(status_code=413, detail="Arquivo excede o limite de 5MB")
    return {"filename": arquivo.filename, "tamanho_bytes": len(conteudo)}