Loading repository data…
Loading repository data…
Santosdevbjj / repository
Dados duplicados em APIs sem controle de integridade | REST assíncrona com FastAPI, PostgreSQL e Repository Pattern | Pipeline CI/CD completo: migrations, lint e testes em Docker isolado
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.
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:
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:
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.
uvicorn --reload com volume montado; prod utiliza gunicorn com 4 workers e restart: unless-stopped.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:
GET /v1/atletas/ aceita filtros por nome (contains) e cpf (exato) com paginação limit/offset, adequado para interfaces de backoffice com grandes volumes.selectinload.make deploy-check.make dev-up e make prod-up são operações distintas e independentes, eliminando o risco de configuração de desenvolvimento em produção.1. Clonar o repositório
git clone https://github.com/Santosdevbjj/api-RESTful-FastAPI-Python.git
cd api-RESTful-FastAPI-Python
2. 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 --reload
4. 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 montado
python -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/1
A 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/
│ │ ├──