Ir para o conteúdo

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ção
  • internal/handlers/auth/auth_test.go - Testes de handlers de auth
  • internal/utils/jwt_test.go - Testes de JWT
  • internal/handlers/plans/plans_test.go - Testes de planos
  • internal/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

Referências