Python

FastAPI: APIs modernas com tipagem e documentação automática Já leu

10 min de leitura

FastAPI: APIs modernas com tipagem e documentação automática
FastAPI é o framework web que mais cresceu no ecossistema Python nos últimos anos — e por boas razões. Ele combina alta performance, tipagem estática, validação automática de dados, documentação interativa gerada automat

FastAPI é o framework web que mais cresceu no ecossistema Python nos últimos anos — e por boas razões. Ele combina alta performance, tipagem estática, validação automática de dados, documentação interativa gerada automaticamente e suporte nativo a async/await. Se Flask é o canivete suíço, FastAPI é o bisturi: preciso, moderno e construído sobre os padrões mais atuais da linguagem.


Instalação

pip install fastapi uvicorn[standard]

uvicorn é o servidor ASGI que executa a aplicação FastAPI. ASGI (Asynchronous Server Gateway Interface) é o sucessor do WSGI, com suporte nativo a async.


Primeiro Endpoint

# main.py
from fastapi import FastAPI

app = FastAPI(
    title="API Escolar",
    description="Sistema de gestão de alunos",
    version="1.0.0"
)

@app.get("/")
def raiz():
    return {"mensagem": "API Escolar online"}

@app.get("/saude")
async def verificar_saude():
    return {"status": "ok"}
uvicorn main:app --reload

Acesse: - http://localhost:8000 — API - http://localhost:8000/docs — Swagger UI interativo gerado automaticamente - http://localhost:8000/redoc — ReDoc alternativo


Pydantic: Validação e Serialização

FastAPI usa Pydantic para validar dados automaticamente. Você define modelos com tipos e o FastAPI cuida do resto:

from pydantic import BaseModel, EmailStr, Field, field_validator
from typing import Optional
from datetime import datetime

class AlunoBase(BaseModel):
    nome:  str     = Field(..., min_length=2, max_length=100,
                           description="Nome completo do aluno")
    email: EmailStr = Field(..., description="E-mail válido")
    nota:  float   = Field(0.0, ge=0.0, le=10.0,
                           description="Nota de 0 a 10")

class AlunoCriar(AlunoBase):
    senha: str = Field(..., min_length=6)

class AlunoAtualizar(BaseModel):
    nome:  Optional[str]   = Field(None, min_length=2)
    email: Optional[EmailStr] = None
    nota:  Optional[float] = Field(None, ge=0.0, le=10.0)

class AlunoResposta(AlunoBase):
    id:        int
    ativo:     bool
    criado_em: datetime

    model_config = {"from_attributes": True}   # permite criar de ORM


class RespostaLista(BaseModel):
    total:  int
    alunos: list[AlunoResposta]


# Validators customizados
class AlunoComValidacao(AlunoBase):
    cpf: str

    @field_validator("cpf")
    @classmethod
    def validar_cpf(cls, v):
        apenas_numeros = "".join(c for c in v if c.isdigit())
        if len(apenas_numeros) != 11:
            raise ValueError("CPF deve ter 11 dígitos.")
        return v

CRUD Completo com FastAPI

from fastapi import FastAPI, HTTPException, status, Query, Path
from pydantic import BaseModel, EmailStr, Field
from typing import Optional
from datetime import datetime

app = FastAPI(title="API Escolar", version="1.0.0")

# Banco em memória
_alunos: dict = {}
_proximo_id: int = 1


class AlunoBase(BaseModel):
    nome:  str      = Field(..., min_length=2, max_length=100)
    email: EmailStr
    nota:  float    = Field(0.0, ge=0.0, le=10.0)

class AlunoCriar(AlunoBase):
    pass

class AlunoAtualizar(BaseModel):
    nome:  Optional[str]      = Field(None, min_length=2)
    email: Optional[EmailStr] = None
    nota:  Optional[float]    = Field(None, ge=0.0, le=10.0)

class AlunoResposta(AlunoBase):
    id:        int
    aprovado:  bool
    criado_em: str


# GET — listar com filtros e paginação
@app.get(
    "/alunos",
    response_model=dict,
    summary="Listar alunos",
    tags=["Alunos"]
)
async def listar_alunos(
    nota_min: float = Query(0.0,  ge=0.0, le=10.0, description="Nota mínima"),
    nota_max: float = Query(10.0, ge=0.0, le=10.0, description="Nota máxima"),
    pagina:   int   = Query(1,    ge=1,             description="Número da página"),
    tamanho:  int   = Query(10,   ge=1, le=100,     description="Itens por página"),
):
    alunos = [
        a for a in _alunos.values()
        if nota_min <= a["nota"] <= nota_max
    ]

    inicio = (pagina - 1) * tamanho
    fim    = inicio + tamanho

    return {
        "total":   len(alunos),
        "pagina":  pagina,
        "tamanho": tamanho,
        "alunos":  alunos[inicio:fim]
    }


# GET — buscar por ID
@app.get(
    "/alunos/{aluno_id}",
    response_model=AlunoResposta,
    summary="Buscar aluno por ID",
    tags=["Alunos"]
)
async def buscar_aluno(
    aluno_id: int = Path(..., ge=1, description="ID do aluno")
):
    aluno = _alunos.get(aluno_id)
    if not aluno:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail=f"Aluno {aluno_id} não encontrado."
        )
    return aluno


# POST — criar
@app.post(
    "/alunos",
    response_model=AlunoResposta,
    status_code=status.HTTP_201_CREATED,
    summary="Criar aluno",
    tags=["Alunos"]
)
async def criar_aluno(dados: AlunoCriar):
    global _proximo_id

    # Verifica e-mail duplicado
    for aluno in _alunos.values():
        if aluno["email"] == dados.email:
            raise HTTPException(
                status_code=status.HTTP_409_CONFLICT,
                detail="E-mail já cadastrado."
            )

    aluno = {
        "id":        _proximo_id,
        "nome":      dados.nome,
        "email":     dados.email,
        "nota":      dados.nota,
        "aprovado":  dados.nota >= 6.0,
        "criado_em": datetime.utcnow().isoformat()
    }
    _alunos[_proximo_id]  = aluno
    _proximo_id          += 1
    return aluno


# PATCH — atualizar parcialmente
@app.patch(
    "/alunos/{aluno_id}",
    response_model=AlunoResposta,
    summary="Atualizar aluno",
    tags=["Alunos"]
)
async def atualizar_aluno(
    aluno_id: int,
    dados: AlunoAtualizar
):
    aluno = _alunos.get(aluno_id)
    if not aluno:
        raise HTTPException(status_code=404, detail="Aluno não encontrado.")

    # Atualiza apenas campos enviados
    atualizados = dados.model_dump(exclude_unset=True)
    aluno.update(atualizados)

    if "nota" in atualizados:
        aluno["aprovado"] = aluno["nota"] >= 6.0

    return aluno


# DELETE
@app.delete(
    "/alunos/{aluno_id}",
    status_code=status.HTTP_204_NO_CONTENT,
    summary="Deletar aluno",
    tags=["Alunos"]
)
async def deletar_aluno(aluno_id: int):
    if aluno_id not in _alunos:
        raise HTTPException(status_code=404, detail="Aluno não encontrado.")
    del _alunos[aluno_id]

Dependências: Dependency Injection

O sistema de injeção de dependências do FastAPI é um de seus recursos mais poderosos:

from fastapi import Depends, Header, HTTPException
from typing import Annotated


# Dependência simples — autenticação por token
async def verificar_token(
    authorization: Annotated[str | None, Header()] = None
):
    if not authorization or not authorization.startswith("Bearer "):
        raise HTTPException(
            status_code=401,
            detail="Token de autenticação obrigatório."
        )
    token = authorization.replace("Bearer ", "")
    if token != "token-secreto-123":
        raise HTTPException(status_code=403, detail="Token inválido.")
    return token


# Dependência de paginação — reutilizável
class Paginacao:
    def __init__(
        self,
        pagina:  int = Query(1,  ge=1),
        tamanho: int = Query(10, ge=1, le=100)
    ):
        self.pagina   = pagina
        self.tamanho  = tamanho
        self.offset   = (pagina - 1) * tamanho


# Usando dependências
@app.get("/admin/alunos", dependencies=[Depends(verificar_token)])
async def listar_admin(paginacao: Annotated[Paginacao, Depends()]):
    alunos = list(_alunos.values())
    inicio = paginacao.offset
    fim    = inicio + paginacao.tamanho
    return {"alunos": alunos[inicio:fim], "pagina": paginacao.pagina}


# Dependência que fornece "banco de dados"
def get_db():
    """Em produção, retornaria uma sessão SQLAlchemy."""
    db = _alunos   # simulando
    try:
        yield db
    finally:
        pass   # fechar conexão aqui


@app.get("/v2/alunos")
async def listar_v2(db: Annotated[dict, Depends(get_db)]):
    return list(db.values())

Middleware e CORS

from fastapi.middleware.cors import CORSMiddleware
from fastapi.middleware.gzip import GZipMiddleware
from starlette.middleware.base import BaseHTTPMiddleware
from starlette.requests import Request
import time


# CORS — essencial para APIs consumidas por frontend
app.add_middleware(
    CORSMiddleware,
    allow_origins=["http://localhost:3000", "https://meusite.com"],
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)

# Compressão automática de respostas grandes
app.add_middleware(GZipMiddleware, minimum_size=1000)


# Middleware customizado
class LoggingMiddleware(BaseHTTPMiddleware):
    async def dispatch(self, request: Request, call_next):
        inicio   = time.perf_counter()
        resposta = await call_next(request)
        duracao  = time.perf_counter() - inicio
        print(f"{request.method} {request.url.path} "
              f"→ {resposta.status_code} "
              f"({duracao*1000:.1f}ms)")
        resposta.headers["X-Tempo-Ms"] = f"{duracao*1000:.2f}"
        return resposta


app.add_middleware(LoggingMiddleware)

Lifespan: Startup e Shutdown

from contextlib import asynccontextmanager
from fastapi import FastAPI


@asynccontextmanager
async def lifespan(app: FastAPI):
    # Startup — executado ao iniciar
    print("Iniciando aplicação...")
    # Aqui: conectar ao banco, carregar modelos ML, etc.
    _alunos.update({
        1: {"id": 1, "nome": "Ana",   "email": "ana@email.com",   "nota": 9.5, "aprovado": True,  "criado_em": "2024-01-01"},
        2: {"id": 2, "nome": "Bruno", "email": "bruno@email.com", "nota": 7.0, "aprovado": True,  "criado_em": "2024-01-02"},
        3: {"id": 3, "nome": "Carla", "email": "carla@email.com", "nota": 5.5, "aprovado": False, "criado_em": "2024-01-03"},
    })
    print(f"Banco carregado com {len(_alunos)} alunos.")

    yield   # aplicação em execução

    # Shutdown — executado ao encerrar
    print("Encerrando aplicação...")
    _alunos.clear()


app = FastAPI(title="API Escolar", lifespan=lifespan)

Background Tasks

from fastapi import BackgroundTasks
from datetime import datetime


def enviar_email_boas_vindas(email: str, nome: str):
    """Executado em background — não bloqueia a resposta."""
    import time
    time.sleep(2)   # simula envio de e-mail
    print(f"[Email] Boas-vindas enviadas para {nome} <{email}>")


def registrar_log(acao: str, dados: dict):
    timestamp = datetime.utcnow().isoformat()
    with open("auditoria.log", "a") as f:
        f.write(f"{timestamp} | {acao} | {dados}\n")


@app.post("/alunos/registro", status_code=201)
async def registrar_aluno(
    dados: AlunoCriar,
    background_tasks: BackgroundTasks
):
    global _proximo_id
    aluno = {
        "id":       _proximo_id,
        "nome":     dados.nome,
        "email":    dados.email,
        "nota":     dados.nota,
        "aprovado": dados.nota >= 6.0,
        "criado_em": datetime.utcnow().isoformat()
    }
    _alunos[_proximo_id]  = aluno
    _proximo_id          += 1

    # Agendando tarefas em background
    background_tasks.add_task(enviar_email_boas_vindas, dados.email, dados.nome)
    background_tasks.add_task(registrar_log, "CRIAR_ALUNO", {"id": aluno["id"]})

    return aluno   # responde imediatamente

Exemplo Completo: Estrutura de Projeto FastAPI

escola-api/
├── app/
│   ├── __init__.py
│   ├── main.py
│   ├── config.py
│   ├── modelos/
│   │   ├── __init__.py
│   │   └── aluno.py        ← modelos Pydantic
│   ├── routers/
│   │   ├── __init__.py
│   │   └── alunos.py       ← endpoints
│   ├── servicos/
│   │   ├── __init__.py
│   │   └── aluno_servico.py ← lógica de negócio
│   └── dependencias.py     ← injeção de dependências
├── tests/
│   └── test_alunos.py
├── .env
├── requirements.txt
└── pyproject.toml
# app/main.py
from fastapi import FastAPI
from app.routers import alunos
from app.config import configuracoes
from contextlib import asynccontextmanager


@asynccontextmanager
async def lifespan(app: FastAPI):
    print(f"Iniciando {configuracoes.nome_app} v{configuracoes.versao}")
    yield
    print("Encerrando aplicação.")


def criar_app() -> FastAPI:
    app = FastAPI(
        title=configuracoes.nome_app,
        version=configuracoes.versao,
        lifespan=lifespan
    )
    app.include_router(alunos.router)
    return app


app = criar_app()
# Testando com pytest
from fastapi.testclient import TestClient
from app.main import app

client = TestClient(app)

def test_listar_alunos():
    resposta = client.get("/alunos")
    assert resposta.status_code == 200
    assert "alunos" in resposta.json()

def test_criar_aluno():
    resposta = client.post("/alunos", json={
        "nome":  "Eduardo",
        "email": "eduardo@email.com",
        "nota":  8.5
    })
    assert resposta.status_code == 201
    assert resposta.json()["nome"] == "Eduardo"

def test_email_duplicado():
    dados = {"nome": "Teste", "email": "duplicado@email.com", "nota": 7.0}
    client.post("/alunos", json=dados)
    resposta = client.post("/alunos", json=dados)
    assert resposta.status_code == 409

Flask vs FastAPI

Característica Flask FastAPI
Curva de aprendizado Baixa Média
Performance Boa Excelente (ASGI)
Tipagem Opcional Nativa
Validação Manual Automática (Pydantic)
Documentação Manual Automática (OpenAPI)
Async nativo Não Sim
Maturidade Alta Alta (desde 2018)
Ecossistema Imenso Crescendo rápido

Resumo

  • FastAPI combina tipagem, validação automática e documentação interativa em um único framework
  • Pydantic valida e serializa dados automaticamente — erros são retornados com detalhes precisos
  • Query, Path, Header e Body declaram e validam parâmetros de forma expressiva
  • O sistema de injeção de dependências (Depends) é reutilizável, testável e elegante
  • CORS, GZip e middlewares customizados são adicionados com add_middleware
  • lifespan substitui startup e shutdown events — gerencia recursos da aplicação
  • BackgroundTasks executa tarefas pesadas sem bloquear a resposta ao cliente
  • TestClient permite testar endpoints sem subir o servidor — integrado com pytest

Referências e Leituras Complementares

  • FastAPI — documentação oficial — https://fastapi.tiangolo.com/
  • Pydantic v2 — documentação — https://docs.pydantic.dev/latest/
  • Uvicorn — servidor ASGI — https://www.uvicorn.org/
  • OpenAPI Specification — https://spec.openapis.org/oas/v3.1.0
  • Starlette — base do FastAPI — https://www.starlette.io/
  • GRINBERG, Miguel. Flask Web Development. 2. ed. O'Reilly Media, 2018. — contexto histórico e comparativo com Flask.
  • LATHKAR, Malhar. FastAPI Essentials. Packt, 2024. — guia prático de FastAPI do básico ao avançado.
Comentários

Mais em Python

Artigo 50 — Projeto 3: Bot de Automação com Scraping e Notificações
Artigo 50 — Projeto 3: Bot de Automação com Scraping e Notificações

O Projeto 3 é um bot de automação completo — monitora fontes de dados na web,...

Laços de Repetição: for e while
Laços de Repetição: for e while

Computadores s&atilde;o excepcionalmente bons em repetir tarefas &mdash; muit...

MySQL, MariaDB, PostgreSQL e MongoDB
MySQL, MariaDB, PostgreSQL e MongoDB

No artigo anterior trabalhamos com SQLite — excelente para desenvolvimento lo...