API RESTful assíncrona para gerenciamento de atletas, categorias e centros de treinamento de CrossFit.
Arquitetura em camadas (Repository + Service + Schema), migrations versionadas, ambientes dev/prod isolados e pipeline CI/CD com GitHub Actions.
- 1. Problema de Negócio
- 2. Contexto
- 3. Premissas da Solução
- 4. Estratégia da Solução
- 5. Technical Insights
- 6. Resultados e Capacidades da API
- 7. Como Executar o Projeto
- 8. Referência de Endpoints
- 9. Estrutura do Projeto
- 10. Pipeline CI/CD
- 11. Aprendizados
- 12. Próximos Passos
- Autor
Competições de CrossFit envolvem múltiplos atletas distribuídos em categorias e centros de treinamento. Gerenciar esse cadastro sem uma API estruturada gera três problemas recorrentes:
- Duplicidade de dados: sem controle de integridade no CPF, o mesmo atleta pode ser cadastrado múltiplas vezes, corrompendo rankings e relatórios de competição.
- Inconsistência de relacionamentos: atletas associados a categorias ou centros inexistentes geram falhas silenciosas em sistemas que consomem esses dados downstream.
- Falta de rastreabilidade: sem migrations versionadas, alterações de schema em produção tornam-se operações manuais e não auditáveis.
O objetivo deste projeto é entregar uma API que previne esses problemas na camada de infraestrutura, liberando as camadas de negócio para focar em regras de domínio.
Este projeto foi desenvolvido no Bootcamp Luizalabs — Back-end com Python, com escopo expandido para demonstrar padrões de produção além do exigido pelo bootcamp.
O domínio modela três entidades centrais de uma plataforma de CrossFit:
- Atleta: entidade principal, identificada unicamente por CPF, associada a uma categoria e um centro de treinamento.
- Categoria: agrupa atletas por nível ou peso (ex: Elite, Rx, Scaled).
- Centro de Treinamento: local físico onde o atleta treina — necessário para logística de competição.
A decisão de usar FastAPI com I/O assíncrono reflete o cenário real: plataformas de esporte recebem picos de requisições no momento de abertura de inscrições, onde latência e throughput são críticos.
- O CPF é a chave de unicidade do atleta — violações de integridade devem retornar mensagem clara com o valor duplicado, não um erro genérico 500.
- Ambientes de desenvolvimento e produção têm requisitos diferentes: dev utiliza
uvicorn --reloadcom volume montado; prod utilizagunicorncom 4 workers erestart: unless-stopped. - Migrations devem ser executadas automaticamente no startup do container, garantindo que o schema esteja sempre sincronizado sem intervenção manual.
- O pipeline CI deve reproduzir o ambiente de produção usando Docker Compose, não mocks ou SQLite em memória.
A arquitetura foi organizada em camadas com responsabilidades bem definidas:
Request → Router → Service → Repository → Model (SQLAlchemy) → PostgreSQL
↓
Schema (Pydantic) — validação de entrada e serialização de saída
↓
Exception Handler — trata IntegrityError antes de virar HTTP 500
Camadas e decisões:
| Camada | Responsabilidade | Decisão técnica |
|---|---|---|
Router (app/api/v1/) |
Recebe HTTP, valida query params, chama Service | FastAPI APIRouter com prefix e tags |
Service (app/services/) |
Orquestra regras de negócio | Isola lógica do repository para facilitar testes |
Repository (app/repositories/) |
Acesso ao banco | selectinload para carregar relacionamentos em uma única query assíncrona |
Schema (app/schemas/) |
Contrato da API | Pydantic v2 com from_attributes=True para serialização ORM |
Model (app/models/) |
Mapeamento ORM | SQLAlchemy 2.0 com ForeignKey e relationship bidirecionais |
Migrations (alembic/) |
Versionamento de schema | async_engine_from_config — migrations rodam no mesmo engine assíncrono da app |
| Exception Handler | Tratamento de integridade | Extrai o CPF duplicado via regex na mensagem do PostgreSQL e retorna HTTP 303 |
Ambientes Docker:
| Arquivo | Propósito |
|---|---|
docker-compose.yml |
Base compartilhada (db + healthcheck) |
docker-compose.override.yml |
Dev: volume montado + uvicorn reload |
docker-compose.prod.yml |
Prod: gunicorn 4 workers + restart policy |
Por que asyncpg em vez de psycopg2?
psycopg2 é síncrono e bloqueia a event loop do Python quando executa queries. Com asyncpg + SQLAlchemy async, a API mantém throughput alto sob carga concorrente — essencial em picos de inscrição de competições.
Por que o IntegrityError retorna HTTP 303 e não 409?
O handler captura a exceção antes que o FastAPI a transforme em 500, extrai o CPF duplicado da mensagem interna do PostgreSQL via regex (Key (cpf)=(valor) already exists) e retorna uma resposta semântica. HTTP 303 foi a escolha do bootcamp para indicar que o recurso já existe em outro local — em produção enterprise, 409 Conflict seria mais aderente ao padrão REST.
Por que selectinload em vez de joinedload?
joinedload gera um JOIN que pode multiplicar linhas quando há múltiplos relacionamentos. selectinload emite queries SELECT ... WHERE id IN (...) separadas, mais previsíveis em termos de cardinalidade e mais eficientes para listas paginadas.
Por que três arquivos Docker Compose em vez de um?
O padrão de override do Docker Compose permite que docker-compose.override.yml seja aplicado automaticamente em dev (sem flag -f) enquanto prod exige a composição explícita docker-compose -f docker-compose.yml -f docker-compose.prod.yml. Isso evita configurações de desenvolvimento vazando para produção.
Makefile como interface de operações:
O Makefile encapsula todos os comandos complexos (make dev-up, make migrate-up, make deploy) eliminando a dependência de conhecimento dos flags do Docker Compose por parte de quem executa o projeto.
A API entrega as seguintes capacidades em ambiente containerizado:
- Cadastro de atletas com validação de unicidade: CPF duplicado retorna mensagem descritiva em vez de erro genérico, reduzindo tempo de debug em integrações.
- Listagem paginada com filtros: endpoint
GET /v1/atletas/aceita filtros pornome(contains) ecpf(exato) com paginaçãolimit/offset, adequado para interfaces de backoffice com grandes volumes. - Relacionamentos carregados sem N+1: a query de listagem carrega categoria e centro de treinamento em uma única roundtrip ao banco via
selectinload. - Pipeline CI reproduzível: o workflow do GitHub Actions sobe o ambiente completo via Docker Compose, roda migrations, lint e testes — o mesmo fluxo que qualquer developer executaria localmente com
make deploy-check. - Ambientes isolados:
make dev-upemake prod-upsão operações distintas e independentes, eliminando o risco de configuração de desenvolvimento em produção.
- Docker e Docker Compose
- Git
- (Opcional) Python 3.11+ para execução local sem Docker
1. Clonar o repositório
git clone https://github.com/Santosdevbjj/api-RESTful-FastAPI-Python.git
cd api-RESTful-FastAPI-Python2. Configurar variáveis de ambiente
cp .env.example .env
# Edite .env conforme necessário (as defaults já funcionam para dev local)3. Subir o ambiente completo
make dev-up
# Equivale a: docker-compose up --build
# Inclui: build da imagem, start do PostgreSQL, healthcheck, migrations automáticas e uvicorn --reload4. Acessar a documentação interativa
Acesse 👉 http://localhost:8000/docs — Swagger UI gerado automaticamente pelo FastAPI.
make prod-up
# Equivale a: docker-compose -f docker-compose.yml -f docker-compose.prod.yml up -d --build
# Gunicorn com 4 UvicornWorkers, restart: unless-stopped, sem volume montadopython -m venv venv
source venv/bin/activate # Windows: venv\Scripts\activate
pip install -r requirements.txt
# Configure DATABASE_URL no .env apontando para um PostgreSQL local
alembic upgrade head
uvicorn app.main:app --reload| Comando | Ação |
|---|---|
make dev-up |
Sobe ambiente de desenvolvimento |
make prod-up |
Sobe ambiente de produção |
make test |
Executa Pytest no container |
make lint |
Roda flake8 + black check |
make migrate-up |
Aplica migrations pendentes |
make migrate-create d="nome" |
Cria nova migration Alembic |
make deploy-check |
Simula pipeline: lint + testes |
make clean |
Remove containers, volumes e imagens |
Criar Atleta
curl -X POST http://localhost:8000/v1/atletas/ \
-H "Content-Type: application/json" \
-d '{
"nome": "João Silva",
"cpf": "12345678901",
"categoria_id": 1,
"centro_id": 1
}'Listar com filtros e paginação
curl "http://localhost:8000/v1/atletas/?nome=João&limit=10&offset=0"Filtrar por CPF
curl "http://localhost:8000/v1/atletas/?cpf=12345678901"Atualizar Atleta
curl -X PUT http://localhost:8000/v1/atletas/1 \
-H "Content-Type: application/json" \
-d '{"nome": "João Souza"}'Deletar Atleta
curl -X DELETE http://localhost:8000/v1/atletas/1A coleção Postman completa está disponível em
postman/apiWorkoutPython.postman_collection.json
Inclui exemplos prontos para Atletas, Categorias e Centros de Treinamento.
api-RESTful-FastAPI-Python/
├── app/
│ ├── api/
│ │ └── v1/
│ │ ├── routes_atletas.py # Endpoints de Atleta (CRUD + paginação)
│ │ ├── routes_categorias.py # Endpoints de Categoria
│ │ └── routes_centros.py # Endpoints de Centro de Treinamento
│ ├── core/
│ │ ├── config.py # Settings via Pydantic BaseSettings + .env
│ │ └── logging.py # Configuração de logging
│ ├── db/
│ │ ├── base.py # DeclarativeBase do SQLAlchemy
│ │ ├── session.py # AsyncEngine + AsyncSession factory
│ │ └── init_db.py # Inicialização de tabelas (dev utility)
│ ├── exceptions/
│ │ └── handlers.py # IntegrityError → HTTP 303 + CPF extraído
│ ├── models/
│ │ ├── atleta.py # ORM: Atleta (cpf unique, FK categoria/centro)
│ │ ├── categoria.py # ORM: Categoria
│ │ └── centro_treinamento.py # ORM: CentroTreinamento
│ ├── repositories/
│ │ └── atleta_repo.py # Queries async com selectinload
│ ├── schemas/
│ │ ├── atleta.py # Pydantic: AtletaCreate, AtletaRead
│ │ ├── categoria.py # Pydantic: CategoriaCreate, CategoriaRead
│ │ └── centro_treinamento.py # Pydantic: CentroCreate, CentroRead
│ ├── services/
│ │ └── atleta_service.py # Orquestração de regras de negócio
│ └── main.py # App FastAPI: routers, CORS, exception handlers
├── alembic/
│ └── env.py # Migrations async com async_engine_from_config
├── docker/
│ ├── Dockerfile # python:3.11-slim
│ └── gunicorn_conf.py # Configuração Gunicorn para produção
├── tests/
│ ├── conftest.py # Fixture AsyncClient
│ └── test_atleta.py # Testes de endpoint
├── postman/
│ └── apiWorkoutPython.postman_collection.json
├── .github/workflows/ci.yml # CI: build → migrations → lint → testes
├── docker-compose.yml # Base: db com healthcheck
├── docker-compose.override.yml # Dev: volume + uvicorn reload
├── docker-compose.prod.yml # Prod: gunicorn 4 workers + restart policy
├── Makefile # Interface de operações
├── requirements.txt
└── .env.example
O workflow .github/workflows/ci.yml executa em cada push para main e develop:
Checkout → Docker Buildx → Cache de layers → docker-compose up
→ pg_isready (healthcheck) → alembic upgrade head
→ black --check + flake8 → pytest -v → docker-compose down -v
A decisão de usar Docker Compose no CI (em vez de rodar Pytest diretamente no runner) garante que o ambiente de teste seja idêntico ao de produção — mesma imagem, mesmo PostgreSQL 15, mesmas variáveis de ambiente.
O que foi mais desafiador: configurar o Alembic para rodar com engine assíncrono (async_engine_from_config) foi o ponto de maior atrito. A documentação oficial do Alembic ainda tem muitos exemplos síncronos, e a integração com asyncpg exigiu entender a diferença entre run_sync e run_migrations_online no contexto assíncrono.
O que faria diferente hoje: o handler de IntegrityError faz parsing da mensagem de erro do PostgreSQL via regex — funciona, mas é frágil se a mensagem mudar entre versões do driver. Uma abordagem mais robusta seria checar a existência do CPF antes do INSERT dentro de uma transação serializable, eliminando a dependência de parsing de strings de erro.
Principal aprendizado arquitetural: separar Repository do Service parece overhead em projetos pequenos, mas o benefício aparece imediatamente nos testes — é possível mockar o AtletaRepository e testar o AtletaService sem tocar no banco. Isso reduz o tempo de execução dos testes unitários em ordens de magnitude.
Sobre Docker Compose com múltiplos arquivos: o padrão base + override + prod é uma das decisões que mais economiza tempo operacional. Não há risco de subir produção com volume montado ou sem política de restart por esquecimento.
- Implementar endpoints completos de Categorias e Centros de Treinamento (atualmente retornam placeholder)
- Adicionar autenticação JWT para proteger endpoints de escrita
- Expandir cobertura de testes com fixtures de banco isolado por teste (PostgreSQL em container dedicado no CI)
- Adicionar endpoint de busca por atleta com paginação cursor-based para suportar volumes maiores
- Implementar cache Redis para listagens frequentes em período de competição
- Configurar observabilidade com OpenTelemetry + traces distribuídos
Sérgio Santos
Senior Data Engineer & Cloud Architect | 15+ anos em sistemas críticos de Banking & Fintech
Distribuído sob a licença MIT. Veja LICENSE para mais detalhes.