APIs REST com FastAPI: Tutorial Completo

Aprenda a construir APIs REST profissionais com FastAPI em Python: rotas, modelos Pydantic, validação, autenticação e documentação automática.

7 min de leitura Equipe Python Brasil

FastAPI é o framework Python que mais cresce para criação de APIs. Ele combina alta performance, documentação automática e validação de dados nativa — tudo isso mantendo a simplicidade que a gente espera de Python. Neste tutorial, você vai construir uma API REST completa do zero.

Por que FastAPI?

Antes de colocar a mão na massa, veja por que FastAPI se destaca:

  • Performance: Uma das mais rápidas em Python (baseado em Starlette e Uvicorn)
  • Documentação automática: Swagger UI e ReDoc gerados automaticamente
  • Validação: Pydantic valida dados de entrada automaticamente
  • Tipagem: Usa type hints do Python para definir esquemas
  • Async: Suporte nativo a async/await
  • Fácil de aprender: Se você sabe Python, já sabe quase tudo

Configuração Inicial

# Instalação
# pip install fastapi uvicorn[standard]

# Para rodar o servidor:
# uvicorn main:app --reload

Hello World com FastAPI

from fastapi import FastAPI

app = FastAPI(
    title="Minha API",
    description="API de exemplo com FastAPI",
    version="1.0.0",
)

@app.get("/")
def raiz():
    return {"mensagem": "Olá, FastAPI!", "status": "online"}

@app.get("/saudacao/{nome}")
def saudacao(nome: str):
    return {"mensagem": f"Olá, {nome}! Bem-vindo à nossa API."}

Acesse http://localhost:8000/docs para ver a documentação interativa automática.

Modelos com Pydantic

Pydantic é o coração da validação de dados no FastAPI:

from pydantic import BaseModel, Field, validator, EmailStr
from typing import Optional
from datetime import datetime
from enum import Enum

class StatusTarefa(str, Enum):
    pendente = "pendente"
    em_andamento = "em_andamento"
    concluida = "concluida"
    cancelada = "cancelada"

class TarefaBase(BaseModel):
    titulo: str = Field(..., min_length=3, max_length=100,
                         description="Título da tarefa")
    descricao: Optional[str] = Field(None, max_length=500)
    prioridade: int = Field(default=1, ge=1, le=5,
                             description="Prioridade de 1 (baixa) a 5 (alta)")

class TarefaCriar(TarefaBase):
    responsavel_email: Optional[str] = None

    @validator("titulo")
    def titulo_capitalizado(cls, v):
        return v.strip().capitalize()

class TarefaAtualizar(BaseModel):
    titulo: Optional[str] = Field(None, min_length=3, max_length=100)
    descricao: Optional[str] = None
    status: Optional[StatusTarefa] = None
    prioridade: Optional[int] = Field(None, ge=1, le=5)

class TarefaResposta(TarefaBase):
    id: int
    status: StatusTarefa = StatusTarefa.pendente
    criada_em: datetime
    atualizada_em: Optional[datetime] = None

    class Config:
        from_attributes = True

CRUD Completo

Vamos construir uma API de gerenciamento de tarefas com todas as operações CRUD:

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

app = FastAPI(title="API de Tarefas", version="2.0.0")

# ---- Modelos ----

class StatusTarefa(str, Enum):
    pendente = "pendente"
    em_andamento = "em_andamento"
    concluida = "concluida"

class TarefaCriar(BaseModel):
    titulo: str = Field(..., min_length=3, max_length=100)
    descricao: Optional[str] = None
    prioridade: int = Field(default=1, ge=1, le=5)

class TarefaAtualizar(BaseModel):
    titulo: Optional[str] = Field(None, min_length=3, max_length=100)
    descricao: Optional[str] = None
    status: Optional[StatusTarefa] = None
    prioridade: Optional[int] = Field(None, ge=1, le=5)

class Tarefa(BaseModel):
    id: int
    titulo: str
    descricao: Optional[str]
    status: StatusTarefa
    prioridade: int
    criada_em: datetime
    atualizada_em: Optional[datetime]

# ---- "Banco de dados" em memória ----

banco_tarefas: dict[int, dict] = {}
proximo_id = 1

# ---- Rotas ----

@app.post("/tarefas", response_model=Tarefa, status_code=status.HTTP_201_CREATED)
def criar_tarefa(tarefa: TarefaCriar):
    """Cria uma nova tarefa."""
    global proximo_id

    nova_tarefa = {
        "id": proximo_id,
        "titulo": tarefa.titulo,
        "descricao": tarefa.descricao,
        "status": StatusTarefa.pendente,
        "prioridade": tarefa.prioridade,
        "criada_em": datetime.now(),
        "atualizada_em": None,
    }

    banco_tarefas[proximo_id] = nova_tarefa
    proximo_id += 1

    return nova_tarefa


@app.get("/tarefas", response_model=list[Tarefa])
def listar_tarefas(
    status: Optional[StatusTarefa] = Query(None, description="Filtrar por status"),
    prioridade_min: Optional[int] = Query(None, ge=1, le=5),
    busca: Optional[str] = Query(None, description="Buscar no título"),
    ordenar_por: str = Query("criada_em", enum=["criada_em", "prioridade"]),
    limite: int = Query(10, ge=1, le=100),
    offset: int = Query(0, ge=0),
):
    """Lista tarefas com filtros opcionais."""
    tarefas = list(banco_tarefas.values())

    # Aplicar filtros
    if status:
        tarefas = [t for t in tarefas if t["status"] == status]
    if prioridade_min:
        tarefas = [t for t in tarefas if t["prioridade"] >= prioridade_min]
    if busca:
        tarefas = [t for t in tarefas if busca.lower() in t["titulo"].lower()]

    # Ordenar
    reverse = ordenar_por == "prioridade"
    tarefas.sort(key=lambda t: t[ordenar_por], reverse=reverse)

    # Paginação
    return tarefas[offset:offset + limite]


@app.get("/tarefas/{tarefa_id}", response_model=Tarefa)
def obter_tarefa(tarefa_id: int = Path(..., gt=0)):
    """Obtém uma tarefa pelo ID."""
    if tarefa_id not in banco_tarefas:
        raise HTTPException(
            status_code=404,
            detail=f"Tarefa {tarefa_id} não encontrada."
        )
    return banco_tarefas[tarefa_id]


@app.put("/tarefas/{tarefa_id}", response_model=Tarefa)
def atualizar_tarefa(tarefa_id: int, atualizacao: TarefaAtualizar):
    """Atualiza uma tarefa existente."""
    if tarefa_id not in banco_tarefas:
        raise HTTPException(status_code=404, detail="Tarefa não encontrada.")

    tarefa = banco_tarefas[tarefa_id]

    dados_atualizacao = atualizacao.model_dump(exclude_unset=True)
    for campo, valor in dados_atualizacao.items():
        tarefa[campo] = valor

    tarefa["atualizada_em"] = datetime.now()
    return tarefa


@app.delete("/tarefas/{tarefa_id}", status_code=status.HTTP_204_NO_CONTENT)
def deletar_tarefa(tarefa_id: int):
    """Deleta uma tarefa."""
    if tarefa_id not in banco_tarefas:
        raise HTTPException(status_code=404, detail="Tarefa não encontrada.")
    del banco_tarefas[tarefa_id]


@app.patch("/tarefas/{tarefa_id}/concluir", response_model=Tarefa)
def concluir_tarefa(tarefa_id: int):
    """Marca uma tarefa como concluída."""
    if tarefa_id not in banco_tarefas:
        raise HTTPException(status_code=404, detail="Tarefa não encontrada.")

    tarefa = banco_tarefas[tarefa_id]
    tarefa["status"] = StatusTarefa.concluida
    tarefa["atualizada_em"] = datetime.now()
    return tarefa

Middleware e Tratamento de Erros

from fastapi import FastAPI, Request
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import JSONResponse
import time
import logging

app = FastAPI()

# CORS - permitir requisições de outros domínios
app.add_middleware(
    CORSMiddleware,
    allow_origins=["http://localhost:3000", "https://meusite.com.br"],
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)

# Middleware de logging e tempo de resposta
@app.middleware("http")
async def log_requests(request: Request, call_next):
    inicio = time.time()
    response = await call_next(request)
    duracao = time.time() - inicio

    logging.info(
        f"{request.method} {request.url.path} "
        f"- Status: {response.status_code} "
        f"- Duração: {duracao:.3f}s"
    )

    response.headers["X-Process-Time"] = str(duracao)
    return response

# Tratamento global de erros
@app.exception_handler(ValueError)
async def value_error_handler(request: Request, exc: ValueError):
    return JSONResponse(
        status_code=400,
        content={
            "erro": "Valor inválido",
            "detalhe": str(exc),
            "path": str(request.url),
        },
    )

Dependências e Autenticação

from fastapi import FastAPI, Depends, HTTPException, status
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
import jwt
from datetime import datetime, timedelta

app = FastAPI()

CHAVE_SECRETA = "minha-chave-super-secreta-mudar-em-producao"
ALGORITMO = "HS256"

security = HTTPBearer()

def criar_token(dados: dict, expiracao_minutos: int = 30):
    """Cria um token JWT."""
    dados_token = dados.copy()
    expira = datetime.utcnow() + timedelta(minutes=expiracao_minutos)
    dados_token.update({"exp": expira})
    return jwt.encode(dados_token, CHAVE_SECRETA, algorithm=ALGORITMO)

def verificar_token(credentials: HTTPAuthorizationCredentials = Depends(security)):
    """Dependência que verifica o token JWT."""
    try:
        payload = jwt.decode(
            credentials.credentials, CHAVE_SECRETA, algorithms=[ALGORITMO]
        )
        return payload
    except jwt.ExpiredSignatureError:
        raise HTTPException(status_code=401, detail="Token expirado")
    except jwt.InvalidTokenError:
        raise HTTPException(status_code=401, detail="Token inválido")

# Simulação de banco de usuários
usuarios_db = {
    "admin@email.com": {
        "nome": "Admin",
        "senha": "admin123",  # Em produção, use hash!
        "role": "admin",
    }
}

class LoginRequest(BaseModel):
    email: str
    senha: str

@app.post("/auth/login")
def login(dados: LoginRequest):
    usuario = usuarios_db.get(dados.email)
    if not usuario or usuario["senha"] != dados.senha:
        raise HTTPException(status_code=401, detail="Credenciais inválidas")

    token = criar_token({
        "sub": dados.email,
        "nome": usuario["nome"],
        "role": usuario["role"],
    })
    return {"access_token": token, "token_type": "bearer"}

@app.get("/perfil")
def meu_perfil(usuario: dict = Depends(verificar_token)):
    """Rota protegida - requer autenticação."""
    return {
        "email": usuario["sub"],
        "nome": usuario["nome"],
        "role": usuario["role"],
    }

@app.get("/admin/dashboard")
def dashboard_admin(usuario: dict = Depends(verificar_token)):
    """Rota protegida - apenas admins."""
    if usuario.get("role") != "admin":
        raise HTTPException(status_code=403, detail="Acesso negado")
    return {"mensagem": "Bem-vindo ao painel admin!", "usuario": usuario["nome"]}

Organizando o Projeto

Para projetos maiores, organize com routers:

# projeto/
# ├── main.py
# ├── routers/
# │   ├── __init__.py
# │   ├── tarefas.py
# │   └── usuarios.py
# ├── models/
# │   ├── __init__.py
# │   └── schemas.py
# ├── services/
# │   └── auth.py
# └── config.py

# routers/tarefas.py
from fastapi import APIRouter, Depends

router = APIRouter(
    prefix="/tarefas",
    tags=["Tarefas"],
    responses={404: {"description": "Não encontrado"}},
)

@router.get("/")
def listar():
    return {"tarefas": []}

@router.post("/")
def criar(tarefa: TarefaCriar):
    return {"id": 1, **tarefa.model_dump()}

# main.py
from fastapi import FastAPI
from routers import tarefas, usuarios

app = FastAPI(title="API Organizada")
app.include_router(tarefas.router)
app.include_router(usuarios.router)

Integração com Banco de Dados

from sqlalchemy import create_engine, Column, Integer, String, DateTime, Enum
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker, Session
from fastapi import Depends
from datetime import datetime

DATABASE_URL = "sqlite:///./tarefas.db"

engine = create_engine(DATABASE_URL, connect_args={"check_same_thread": False})
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
Base = declarative_base()

# Modelo do banco
class TarefaDB(Base):
    __tablename__ = "tarefas"

    id = Column(Integer, primary_key=True, index=True)
    titulo = Column(String(100), nullable=False)
    descricao = Column(String(500))
    status = Column(String(20), default="pendente")
    prioridade = Column(Integer, default=1)
    criada_em = Column(DateTime, default=datetime.now)

Base.metadata.create_all(bind=engine)

# Dependência para sessão do banco
def get_db():
    db = SessionLocal()
    try:
        yield db
    finally:
        db.close()

# Rota usando o banco
@app.get("/tarefas")
def listar_tarefas(db: Session = Depends(get_db)):
    return db.query(TarefaDB).all()

@app.post("/tarefas", status_code=201)
def criar_tarefa(tarefa: TarefaCriar, db: Session = Depends(get_db)):
    nova = TarefaDB(**tarefa.model_dump())
    db.add(nova)
    db.commit()
    db.refresh(nova)
    return nova

Deploy

Para colocar sua API em produção, você tem várias opções populares no Brasil:

# 1. Usando Gunicorn + Uvicorn (recomendado para produção)
# pip install gunicorn
# gunicorn main:app -w 4 -k uvicorn.workers.UvicornWorker

# 2. Dockerfile
# FROM python:3.12-slim
# WORKDIR /app
# COPY requirements.txt .
# RUN pip install -r requirements.txt
# COPY . .
# CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]

Conclusão

FastAPI é uma escolha excelente para construir APIs modernas em Python. Os principais pontos fortes são a validação automática com Pydantic, a documentação interativa gratuita e a performance de primeiro nível.

Para ir além, explore:

  1. WebSockets para comunicação em tempo real
  2. Background Tasks para processamento assíncrono
  3. Testes com pytest e TestClient do FastAPI
  4. Docker para containerização
  5. CI/CD com GitHub Actions

FastAPI está crescendo rapidamente no mercado brasileiro, especialmente em startups e fintechs. Investir nesse framework é investir no futuro da sua carreira como desenvolvedor Python.

E

Equipe Python Brasil

Contribuidor do Python Brasil — Aprenda Python em Português