Backend - Padrões Arquiteturais¶
Padrões Adotados¶
1. Clean Architecture (Simplificada)¶
O backend segue uma arquitetura em camadas com separação clara de responsabilidades:
┌─────────────────┐
│ Handlers │ ← Camada de apresentação (HTTP)
├─────────────────┤
│ Services │ ← Camada de lógica de negócio
├─────────────────┤
│ Repositories │ ← Camada de acesso a dados
├─────────────────┤
│ Models │ ← Camada de dados
└─────────────────┘
Princípios: - Handlers não conhecem detalhes de implementação de repositories - Services não conhecem detalhes HTTP - Repositories abstraem acesso ao banco
2. Repository Pattern¶
Repositories abstraem acesso a dados:
type ProfessionalProfileRepository interface {
Create(profile *models.ProfessionalProfile) error
GetByID(id uint) (*models.ProfessionalProfile, error)
GetByUserID(userID uint) (*models.ProfessionalProfile, error)
Update(profile *models.ProfessionalProfile) error
}
Implementação:
- GormProfessionalProfileRepository - Implementação com GORM
- Facilita testes (mock de repositories)
- Permite trocar implementação sem afetar services
3. Service Layer Pattern¶
Services concentram lógica de negócio:
type ProfessionalProfileService struct {
repo ProfessionalProfileRepository
userRepo AuthRepository
}
func (s *ProfessionalProfileService) CreateProfile(userID uint, data CreateProfileDTO) error {
// Validações de negócio
// Lógica de criação
// Chamadas a repositories
}
Responsabilidades: - Validações de regras de negócio - Orquestração de múltiplos repositories - Cálculos e transformações - Não conhece detalhes HTTP
4. Handler Pattern¶
Handlers são responsáveis apenas por HTTP:
func (h *Handler) CreateProfile(c echo.Context) error {
// Extrair dados da requisição
var dto CreateProfileDTO
if err := c.Bind(&dto); err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "Invalid request")
}
// Chamar service
err := h.service.CreateProfile(userID, dto)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
}
// Retornar resposta
return c.JSON(http.StatusCreated, result)
}
Responsabilidades: - Validação de formato (JSON, parâmetros) - Extração de dados do contexto (user_id, etc.) - Chamada a services - Formatação de respostas HTTP - Tratamento de erros HTTP
Convenções de Código¶
Nomenclatura¶
Pacotes:
- handlers/ - Handlers HTTP
- services/ - Services de negócio
- repositories/ - Repositories
- models/ - Modelos de dados
- middleware/ - Middlewares
Arquivos:
- *_handler.go - Handlers
- *_service.go - Services
- *_repository.go - Repositories
- *.go - Models (um arquivo por modelo)
Funções:
- Handlers: GetX, CreateX, UpdateX, DeleteX
- Services: Métodos específicos do domínio
- Repositories: Create, GetByID, Update, Delete
Estrutura de Handlers¶
type Handler struct {
service ServiceInterface
}
func NewHandler(service ServiceInterface) *Handler {
return &Handler{service: service}
}
func (h *Handler) GetX(c echo.Context) error {
// Implementação
}
Estrutura de Services¶
type Service struct {
repo RepositoryInterface
}
func NewService(repo RepositoryInterface) *Service {
return &Service{repo: repo}
}
func (s *Service) DoSomething() error {
// Implementação
}
Estrutura de Repositories¶
type Repository struct {
db *gorm.DB
}
func NewRepository(db *gorm.DB) *Repository {
return &Repository{db: db}
}
func (r *Repository) Create(entity *models.Entity) error {
return r.db.Create(entity).Error
}
Estratégias de Versionamento¶
API Versionamento¶
Status atual: Não há versionamento explícito na URL
Estrutura atual:
- Todas as rotas começam com /api/
- Não há /api/v1/ ou similar
Recomendação futura:
- Quando necessário, adicionar /api/v1/, /api/v2/, etc.
- Manter versões antigas por período de transição
Banco de Dados¶
Migrações:
- Migrações SQL manuais em migrations/
- Numeração sequencial: 001_, 002_, etc.
- Rollback scripts em migrations/rollback/
Auto-migrate: - GORM AutoMigrate cria/atualiza tabelas - Não altera tipos de colunas existentes - Migrações manuais para alterações de tipo
Uso de Testes¶
Estrutura de Testes¶
Arquivos de teste:
- *_test.go - Arquivos de teste
- Mesmo pacote do código testado
Exemplo:
// auth_test.go
package auth
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestAuthMiddleware(t *testing.T) {
// Teste
}
Testes Existentes¶
internal/middleware/auth/auth_test.go- Testes de autenticaçãointernal/handlers/auth/auth_test.go- Testes de handlers de authinternal/utils/jwt_test.go- Testes de JWTinternal/handlers/plans/plans_test.go- Testes de planosinternal/handlers/admin/users_test.go- Testes de usuários
Mock de Banco de Dados¶
Ferramenta: github.com/DATA-DOG/go-sqlmock
Uso: - Mock de queries SQL - Testes de repositories sem banco real - Testes mais rápidos
Boas Práticas Obrigatórias¶
1. Validação de Entrada¶
Sempre validar: - Formato de JSON - Tipos de dados - Campos obrigatórios - Valores válidos (ranges, enums, etc.)
Exemplo:
if dto.Email == "" {
return echo.NewHTTPError(http.StatusBadRequest, "Email is required")
}
2. Tratamento de Erros¶
Padrão:
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
}
Erros específicos:
- 400 Bad Request - Dados inválidos
- 401 Unauthorized - Não autenticado
- 403 Forbidden - Sem permissão
- 404 Not Found - Recurso não encontrado
- 500 Internal Server Error - Erro do servidor
3. Logging¶
Usar log padrão do Go:
import "log"
log.Println("Info message")
log.Printf("Formatted: %s", value)
log.Fatal("Fatal error") // Encerra aplicação
Níveis:
- log.Println - Informações gerais
- log.Printf - Informações formatadas
- log.Fatal - Erros fatais (encerra aplicação)
4. Contexto do Echo¶
Extrair dados do contexto:
userID := c.Get("user_id").(uint)
userType := c.Get("user_type").(string)
Sempre verificar:
userID, ok := c.Get("user_id").(uint)
if !ok {
return echo.NewHTTPError(http.StatusUnauthorized, "User ID not found")
}
5. Soft Deletes¶
GORM soft deletes:
type Model struct {
DeletedAt gorm.DeletedAt `gorm:"index" json:"deleted_at,omitempty"`
}
Queries automáticas:
- db.Find(&users) - Não retorna deletados
- db.Unscoped().Find(&users) - Inclui deletados
6. Transações¶
Quando usar: - Múltiplas operações que devem ser atômicas - Criar entidades relacionadas
Exemplo:
err := db.Transaction(func(tx *gorm.DB) error {
if err := tx.Create(&user).Error; err != nil {
return err
}
if err := tx.Create(&profile).Error; err != nil {
return err
}
return nil
})
7. Preload de Relacionamentos¶
Evitar N+1 queries:
db.Preload("User").Preload("Observations").Find(&cycles)
Usar quando necessário: - Relacionamentos que serão usados na resposta - Evitar carregar relacionamentos desnecessários
8. Validação de Permissões¶
Sempre verificar: - Role do usuário (via middleware) - Propriedade do recurso (usuário pode acessar apenas seus próprios dados) - Aprovação de perfil (profissionais precisam ter perfil aprovado)
Exemplo:
if cycle.UserID != userID {
return echo.NewHTTPError(http.StatusForbidden, "Access denied")
}
Decisões Pendentes¶
- Container de DI: Considerar Wire ou dig para injeção de dependências
- Validação estruturada: Considerar biblioteca de validação (go-playground/validator)
- Logging estruturado: Considerar zerolog ou logrus
- Rate limiting: Implementar rate limiting para APIs públicas
- Caching: Considerar Redis para cache de queries frequentes