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 chega"] --> B["Middleware 1: Correlation-ID"]
    B --> C["Middleware 2: X-Process-Time"]
    C --> D["Middleware 3: CORSMiddleware"]
    D --> E["Roteamento / Handler da Rota"]
    E -->|"exceção customizada"| F["@app.exception_handler"]
    E -->|"sucesso"| G["Response"]
    F --> H["JSONResponse padronizada"]
    H --> D
    G --> D
    D --> C
    C --> B
    B --> I["Resposta final ao cliente"]

    style A fill:#e1f5fe,stroke:#03a9f4,stroke-width:2px
    style D fill:#fff3e0,stroke:#ff9800,stroke-width:2px
    style E fill:#ede7f6,stroke:#7e57c2,stroke-width:2px
    style F fill:#ffebee,stroke:#e53935,stroke-width:2px
    style G fill:#e8f5e9,stroke:#4caf50,stroke-width:2px
    style I fill:#f3e5f5,stroke:#9c27b0,stroke-width:2px

🏛️ 1. Middlewares, Exception Handlers e CORS: a casca transversal da API

Middlewares e exception handlers resolvem o mesmo problema por ângulos diferentes: evitar repetir lógica transversal em cada rota. Um middleware envolve toda requisição/resposta (logging, tempo de processamento, CORS); um exception handler intercepta um tipo específico de erro onde quer que ele seja levantado, centralizando a formatação da resposta de falha.

Anatomia de um middleware HTTP: call_next como bisonte de duas cabeças

Um middleware registrado com @app.middleware("http") recebe a Request e uma função call_next que, quando awaited, executa o restante da pilha (outros middlewares + a rota) e retorna a Response. Isso dá ao middleware dois pontos de interceptação na mesma função: antes de call_next (pode inspecionar/modificar a requisição) e depois (pode inspecionar/modificar a resposta):

import time
from fastapi import FastAPI, Request

app = FastAPI()

@app.middleware("http")
async def adicionar_tempo_processamento(request: Request, call_next):
    inicio = time.perf_counter()
    response = await call_next(request)
    duracao = time.perf_counter() - inicio
    response.headers["X-Process-Time"] = f"{duracao:.4f}"
    return response

A ordem de registro importa: middlewares são empilhados como uma cebola — o último registrado é o primeiro a processar a requisição de entrada e o último a processar a resposta de saída, já que ele “envolve” os middlewares registrados antes dele.

Correlation ID: rastreabilidade ponta a ponta

Em arquiteturas com múltiplos serviços, correlacionar logs de uma mesma requisição que atravessa vários processos exige um identificador único propagado ao longo de toda a cadeia. O padrão é extrair (ou gerar, se ausente) um X-Correlation-ID no primeiro middleware, injetá-lo no contexto de logging da aplicação e devolvê-lo no header de resposta, para que o cliente e os serviços downstream compartilhem a mesma referência:

import uuid
from fastapi import FastAPI, Request

app = FastAPI()

@app.middleware("http")
async def correlation_id_middleware(request: Request, call_next):
    correlation_id = request.headers.get("X-Correlation-ID", str(uuid.uuid4()))
    request.state.correlation_id = correlation_id
    response = await call_next(request)
    response.headers["X-Correlation-ID"] = correlation_id
    return response

request.state é o mecanismo correto para passar dados de um middleware para os handlers de rota dentro do mesmo ciclo de requisição — não existe variável global segura para isso em um servidor ASGI concorrente.

Exception handlers: erros de negócio viram respostas HTTP padronizadas

Levantar HTTPException em toda regra de negócio espalha a formatação de erro por dezenas de arquivos. A alternativa é definir exceções de domínio puras (sem qualquer conhecimento de HTTP) e registrar um @app.exception_handler que as traduz centralmente para uma JSONResponse:

from fastapi import FastAPI, Request
from fastapi.responses import JSONResponse

class RecursoNaoEncontradoException(Exception):
    def __init__(self, mensagem: str) -> None:
        self.mensagem = mensagem

app = FastAPI()

@app.exception_handler(RecursoNaoEncontradoException)
async def recurso_nao_encontrado_handler(request: Request, exc: RecursoNaoEncontradoException):
    return JSONResponse(
        status_code=404,
        content={"erro": "RECURSO_NAO_ENCONTRADO", "detalhe": exc.mensagem},
    )

Isso mantém a camada de serviço (Service Layer) desacoplada do protocolo HTTP: uma função de domínio pode levantar RecursoNaoEncontradoException e ser reutilizada tanto por uma rota HTTP quanto por um worker de fila, sem carregar fastapi.HTTPException como dependência.

Uma decisão de design relevante: @app.exception_handler roda fora da cadeia normal de middlewares em versões mais antigas do Starlette (o erro é tratado antes de propagar pela pilha), o que significa que um middleware pode não ver a resposta de erro a menos que use BaseHTTPMiddleware ou trate a exceção via try/except explícito ao redor de call_next. Isso é uma armadilha comum ao combinar logging de resposta com exception handlers customizados — vale sempre testar o caminho de erro, não só o de sucesso.

CORS: middleware, não exception handler

CORS (Cross-Origin Resource Sharing) é resolvido inteiramente no navegador do cliente, mas o servidor precisa declarar explicitamente quem tem permissão de consumir a API a partir de outra origem, via headers de resposta. Em produção, allow_origins=["*"] combinado com allow_credentials=True é uma combinação inválida e insegura (a especificação CORS proíbe curinga com credenciais) — a lista de origens deve ser explícita:

from fastapi.middleware.cors import CORSMiddleware

app.add_middleware(
    CORSMiddleware,
    allow_origins=["https://app.minhaempresa.com"],
    allow_credentials=True,
    allow_methods=["GET", "POST", "PUT", "DELETE"],
    allow_headers=["Authorization", "Content-Type"],
)