Projeto final: API REST com autenticação, testes e Docker

1. Estrutura do Projeto e Configuração Inicial

Para construir uma API REST robusta em Golang, a organização do código é fundamental. Vamos adotar uma estrutura modular que separa claramente responsabilidades:

meu-projeto/
├── cmd/
│   └── server/
│       └── main.go
├── internal/
│   ├── config/
│   ├── database/
│   ├── handlers/
│   ├── middleware/
│   ├── models/
│   ├── services/
│   └── routes/
├── pkg/
│   └── utils/
├── migrations/
├── tests/
├── .env
├── Dockerfile
├── docker-compose.yml
├── go.mod
└── go.sum

Inicializamos o módulo Go e instalamos as dependências essenciais:

// go.mod
module github.com/seuusuario/api-rest-go

go 1.22

require (
    github.com/gin-gonic/gin v1.9.1
    github.com/golang-jwt/jwt/v5 v5.2.0
    github.com/spf13/viper v1.18.1
    gorm.io/gorm v1.25.5
    gorm.io/driver/postgres v1.5.4
    golang.org/x/crypto v0.17.0
    github.com/stretchr/testify v1.8.4
    github.com/testcontainers/testcontainers-go v0.27.0
    github.com/swaggo/swag v1.16.2
    github.com/swaggo/gin-swagger v1.6.0
)

Configuramos variáveis de ambiente com Viper:

// internal/config/config.go
package config

import "github.com/spf13/viper"

type Config struct {
    DBHost     string `mapstructure:"DB_HOST"`
    DBPort     string `mapstructure:"DB_PORT"`
    DBUser     string `mapstructure:"DB_USER"`
    DBPassword string `mapstructure:"DB_PASSWORD"`
    DBName     string `mapstructure:"DB_NAME"`
    JWTSecret  string `mapstructure:"JWT_SECRET"`
    ServerPort string `mapstructure:"SERVER_PORT"`
}

func LoadConfig() (*Config, error) {
    viper.SetConfigFile(".env")
    viper.AutomaticEnv()

    if err := viper.ReadInConfig(); err != nil {
        return nil, err
    }

    var config Config
    if err := viper.Unmarshal(&config); err != nil {
        return nil, err
    }
    return &config, nil
}

2. Modelagem de Dados e Migrações

Definimos os modelos com GORM, incluindo relacionamentos e índices:

// internal/models/user.go
package models

import "gorm.io/gorm"

type User struct {
    gorm.Model
    Name     string    `gorm:"not null;index:idx_name"`
    Email    string    `gorm:"uniqueIndex;not null"`
    Password string    `gorm:"not null"`
    Role     string    `gorm:"default:'user';index"`
    Products []Product `gorm:"foreignKey:UserID"`
    Orders   []Order   `gorm:"foreignKey:UserID"`
}

// internal/models/product.go
type Product struct {
    gorm.Model
    Name        string  `gorm:"not null;index:idx_product_name"`
    Description string
    Price       float64 `gorm:"not null;check:price > 0"`
    Stock       int     `gorm:"not null;default:0;check:stock >= 0"`
    UserID      uint    `gorm:"index"`
    Category    string  `gorm:"index"`
}

// internal/models/order.go
type Order struct {
    gorm.Model
    UserID     uint          `gorm:"index;not null"`
    Status     string        `gorm:"default:'pending';index"`
    Total      float64       `gorm:"not null"`
    Items      []OrderItem   `gorm:"foreignKey:OrderID"`
}

type OrderItem struct {
    gorm.Model
    OrderID   uint    `gorm:"index;not null"`
    ProductID uint    `gorm:"index;not null"`
    Quantity  int     `gorm:"not null;check:quantity > 0"`
    Price     float64 `gorm:"not null"`
}

Implementamos migrações automáticas e seeds:

// internal/database/migration.go
package database

import (
    "log"
    "gorm.io/gorm"
    "golang.org/x/crypto/bcrypt"
    "meu-projeto/internal/models"
)

func RunMigrations(db *gorm.DB) {
    err := db.AutoMigrate(
        &models.User{},
        &models.Product{},
        &models.Order{},
        &models.OrderItem{},
    )
    if err != nil {
        log.Fatal("Failed to run migrations:", err)
    }
    log.Println("Migrations completed successfully")
}

func SeedData(db *gorm.DB) {
    // Criar usuário admin padrão
    hashedPassword, _ := bcrypt.GenerateFromPassword([]byte("admin123"), bcrypt.DefaultCost)
    admin := models.User{
        Name:     "Admin",
        Email:    "admin@example.com",
        Password: string(hashedPassword),
        Role:     "admin",
    }
    db.FirstOrCreate(&admin, models.User{Email: "admin@example.com"})
}

3. Implementação de Autenticação JWT

Criamos o serviço de autenticação com JWT e bcrypt:

// internal/services/auth_service.go
package services

import (
    "errors"
    "time"
    "github.com/golang-jwt/jwt/v5"
    "golang.org/x/crypto/bcrypt"
    "gorm.io/gorm"
    "meu-projeto/internal/config"
    "meu-projeto/internal/models"
)

type AuthService struct {
    db     *gorm.DB
    config *config.Config
}

type Claims struct {
    UserID uint   `json:"user_id"`
    Role   string `json:"role"`
    jwt.RegisteredClaims
}

func NewAuthService(db *gorm.DB, config *config.Config) *AuthService {
    return &AuthService{db: db, config: config}
}

func (s *AuthService) Signup(user *models.User) error {
    hashedPassword, err := bcrypt.GenerateFromPassword([]byte(user.Password), bcrypt.DefaultCost)
    if err != nil {
        return err
    }
    user.Password = string(hashedPassword)
    return s.db.Create(user).Error
}

func (s *AuthService) Login(email, password string) (string, error) {
    var user models.User
    if err := s.db.Where("email = ?", email).First(&user).Error; err != nil {
        return "", errors.New("invalid credentials")
    }

    if err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(password)); err != nil {
        return "", errors.New("invalid credentials")
    }

    claims := &Claims{
        UserID: user.ID,
        Role:   user.Role,
        RegisteredClaims: jwt.RegisteredClaims{
            ExpiresAt: jwt.NewNumericDate(time.Now().Add(24 * time.Hour)),
            IssuedAt:  jwt.NewNumericDate(time.Now()),
        },
    }

    token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
    return token.SignedString([]byte(s.config.JWTSecret))
}

Middleware de autenticação:

// internal/middleware/auth.go
package middleware

import (
    "net/http"
    "strings"
    "github.com/gin-gonic/gin"
    jwt "github.com/golang-jwt/jwt/v5"
    "meu-projeto/internal/config"
    "meu-projeto/internal/services"
)

func AuthMiddleware(config *config.Config) gin.HandlerFunc {
    return func(c *gin.Context) {
        authHeader := c.GetHeader("Authorization")
        if authHeader == "" {
            c.JSON(http.StatusUnauthorized, gin.H{"error": "authorization header required"})
            c.Abort()
            return
        }

        tokenString := strings.Replace(authHeader, "Bearer ", "", 1)
        claims := &services.Claims{}

        token, err := jwt.ParseWithClaims(tokenString, claims, func(token *jwt.Token) (interface{}, error) {
            return []byte(config.JWTSecret), nil
        })

        if err != nil || !token.Valid {
            c.JSON(http.StatusUnauthorized, gin.H{"error": "invalid token"})
            c.Abort()
            return
        }

        c.Set("user_id", claims.UserID)
        c.Set("role", claims.Role)
        c.Next()
    }
}

func AdminOnly() gin.HandlerFunc {
    return func(c *gin.Context) {
        role := c.GetString("role")
        if role != "admin" {
            c.JSON(http.StatusForbidden, gin.H{"error": "admin access required"})
            c.Abort()
            return
        }
        c.Next()
    }
}

4. Desenvolvimento da API REST

Implementamos handlers com paginação e filtros:

// internal/handlers/product_handler.go
package handlers

import (
    "net/http"
    "strconv"
    "github.com/gin-gonic/gin"
    "gorm.io/gorm"
    "meu-projeto/internal/models"
)

type ProductHandler struct {
    db *gorm.DB
}

func NewProductHandler(db *gorm.DB) *ProductHandler {
    return &ProductHandler{db: db}
}

func (h *ProductHandler) List(c *gin.Context) {
    page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
    limit, _ := strconv.Atoi(c.DefaultQuery("limit", "10"))
    category := c.Query("category")
    minPrice, _ := strconv.ParseFloat(c.DefaultQuery("min_price", "0"), 64)

    query := h.db.Model(&models.Product{})

    if category != "" {
        query = query.Where("category = ?", category)
    }
    if minPrice > 0 {
        query = query.Where("price >= ?", minPrice)
    }

    var products []models.Product
    var total int64
    query.Count(&total)

    offset := (page - 1) * limit
    query.Offset(offset).Limit(limit).Find(&products)

    c.JSON(http.StatusOK, gin.H{
        "data":       products,
        "total":      total,
        "page":       page,
        "limit":      limit,
        "totalPages": (total + int64(limit) - 1) / int64(limit),
    })
}

func (h *ProductHandler) Create(c *gin.Context) {
    var product models.Product
    if err := c.ShouldBindJSON(&product); err != nil {
        c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
        return
    }

    userID := c.GetUint("user_id")
    product.UserID = userID

    if err := h.db.Create(&product).Error; err != nil {
        c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to create product"})
        return
    }

    c.JSON(http.StatusCreated, product)
}

5. Testes de Unidade e Integração

Testes unitários com mocks:

// tests/unit/product_handler_test.go
package tests

import (
    "bytes"
    "encoding/json"
    "net/http"
    "net/http/httptest"
    "testing"
    "github.com/gin-gonic/gin"
    "github.com/stretchr/testify/assert"
    "github.com/stretchr/testify/mock"
    "gorm.io/gorm"
    "meu-projeto/internal/handlers"
)

type MockDB struct {
    mock.Mock
}

func (m *MockDB) Create(value interface{}) *gorm.DB {
    args := m.Called(value)
    return args.Get(0).(*gorm.DB)
}

func TestCreateProduct(t *testing.T) {
    gin.SetMode(gin.TestMode)

    // Configurar mock
    mockDB := new(MockDB)
    handler := handlers.NewProductHandler(mockDB)

    // Configurar rota
    r := gin.New()
    r.POST("/products", handler.Create)

    // Corpo da requisição
    product := map[string]interface{}{
        "name":        "Test Product",
        "description": "Test Description",
        "price":       29.99,
        "stock":       100,
        "category":    "electronics",
    }
    jsonBody, _ := json.Marshal(product)

    req, _ := http.NewRequest("POST", "/products", bytes.NewBuffer(jsonBody))
    req.Header.Set("Content-Type", "application/json")

    // Executar
    w := httptest.NewRecorder()
    r.ServeHTTP(w, req)

    // Verificar
    assert.Equal(t, http.StatusCreated, w.Code)
}

Testes de integração com testcontainers:

// tests/integration/db_test.go
package tests

import (
    "context"
    "testing"
    "github.com/testcontainers/testcontainers-go"
    "github.com/testcontainers/testcontainers-go/wait"
    "gorm.io/driver/postgres"
    "gorm.io/gorm"
)

func TestIntegrationDB(t *testing.T) {
    ctx := context.Background()

    req := testcontainers.ContainerRequest{
        Image:        "postgres:16-alpine",
        ExposedPorts: []string{"5432/tcp"},
        Env: map[string]string{
            "POSTGRES_USER":     "test",
            "POSTGRES_PASSWORD": "test",
            "POSTGRES_DB":       "testdb",
        },
        WaitingFor: wait.ForLog("database system is ready to accept connections"),
    }

    postgresContainer, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{
        ContainerRequest: req,
        Started:          true,
    })
    assert.NoError(t, err)
    defer postgresContainer.Terminate(ctx)

    host, _ := postgresContainer.Host(ctx)
    port, _ := postgresContainer.MappedPort(ctx, "5432")

    dsn := "host=" + host + " user=test password=test dbname=testdb port=" + port.Port() + " sslmode=disable"
    db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{})
    assert.NoError(t, err)

    // Executar migrações e testes
    database.RunMigrations(db)
    database.SeedData(db)

    var count int64
    db.Model(&models.User{}).Count(&count)
    assert.Greater(t, count, int64(0))
}

6. Dockerização da Aplicação

Dockerfile multi-stage otimizado:

# Dockerfile
# Estágio 1: Build
FROM golang:1.22-alpine AS builder

WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download

COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -o /app/server ./cmd/server

# Estágio 2: Runtime
FROM alpine:3.19

RUN apk --no-cache add ca-certificates tzdata
WORKDIR /app

COPY --from=builder /app/server .
COPY --from=builder /app/.env .

EXPOSE 8080

HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
    CMD wget --no-verbose --tries=1 --spider http://localhost:8080/health || exit 1

CMD ["./server"]

Docker Compose completo:

# docker-compose.yml
version: '3.8'

services:
  api:
    build: .
    ports:
      - "8080:8080"
    environment:
      DB_HOST: db
      DB_PORT: 5432
      DB_USER: app
      DB_PASSWORD: app_secret
      DB_NAME: api_db
      JWT_SECRET: sua_chave_secreta_jwt_aqui
      SERVER_PORT: 8080
    depends_on:
      db:
        condition: service_healthy
      redis:
        condition: service_started
    networks:
      - app-network
    restart: unless-stopped

  db:
    image: postgres:16-alpine
    environment:
      POSTGRES_USER: app
      POSTGRES_PASSWORD: app_secret
      POSTGRES_DB: api_db
    volumes:
      - postgres_data:/var/lib/postgresql/data
    ports:
      - "5432:5432"
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U app -d api_db"]
      interval: 10s
      timeout: 5s
      retries: 5
    networks:
      - app-network

  redis:
    image: redis:7-alpine
    ports:
      - "6379:6379"
    networks:
      - app-network

networks:
  app-network:
    driver: bridge

volumes:
  postgres_data:

7. Documentação e Deploy

Geramos documentação OpenAPI com swaggo:

// cmd/server/main.go
package main

import (
    "github.com/gin-gonic/gin"
    swaggerFiles "github.com/swaggo/files"
    ginSwagger "github.com/swaggo/gin-swagger"
    "meu-projeto/internal/config"
    "meu-projeto/internal/database"
    "meu-projeto/internal/handlers"
    "meu-projeto/internal/middleware"
    "meu-projeto/internal/services"

    _ "meu-projeto/docs" // swagger docs
)

// @title API REST Go
// @version 1.0
// @description API REST com autenticação JWT, testes e Docker
// @host localhost:8080
// @BasePath /api/v

1

// @securityDefinitions.apikey BearerAuth
// @in header
// @name Authorization

func main() {
    // Configuração inicial
    cfg := config.Load()
    db := database.Connect(cfg)
    database.RunMigrations(db)
    database.SeedData(db)

    // Inicialização de serviços
    authService := services.NewAuthService(db, cfg.JWTSecret)
    userService := services.NewUserService(db)
    productService := services.NewProductService(db)
    orderService := services.NewOrderService(db)

    // Inicialização de handlers
    authHandler := handlers.NewAuthHandler(authService)
    userHandler := handlers.NewUserHandler(userService)
    productHandler := handlers.NewProductHandler(productService)
    orderHandler := handlers.NewOrderHandler(orderService)

    // Configuração do router
    r := gin.Default()

    // Documentação Swagger
    r.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler))

    // Rotas públicas
    r.POST("/api/v1/auth/signup", authHandler.Signup)
    r.POST("/api/v1/auth/login", authHandler.Login)
    r.GET("/health", func(c *gin.Context) {
        c.JSON(200, gin.H{"status": "ok"})
    })

    // Rotas protegidas
    protected := r.Group("/api/v1")
    protected.Use(middleware.AuthMiddleware(cfg.JWTSecret))
    {
        // Usuários
        protected.GET("/users", middleware.AdminOnly(), userHandler.List)
        protected.GET("/users/:id", userHandler.GetByID)
        protected.PUT("/users/:id", userHandler.Update)
        protected.DELETE("/users/:id", middleware.AdminOnly(), userHandler.Delete)

        // Produtos
        protected.GET("/products", productHandler.List)
        protected.GET("/products/:id", productHandler.GetByID)
        protected.POST("/products", productHandler.Create)
        protected.PUT("/products/:id", productHandler.Update)
        protected.DELETE("/products/:id", productHandler.Delete)

        // Pedidos
        protected.GET("/orders", orderHandler.List)
        protected.GET("/orders/:id", orderHandler.GetByID)
        protected.POST("/orders", orderHandler.Create)
        protected.PUT("/orders/:id/cancel", orderHandler.Cancel)
    }

    r.Run(":" + cfg.ServerPort)
}

Scripts de inicialização para produção:

#!/bin/bash
# scripts/deploy.sh

set -e

echo "🚀 Iniciando deploy da API REST..."

# Build da imagem Docker
echo "📦 Buildando imagem Docker..."
docker build -t sua-imagem/api-rest:latest .

# Push para Docker Hub
echo "📤 Enviando imagem para Docker Hub..."
docker push sua-imagem/api-rest:latest

# Deploy com Docker Compose
echo "🐳 Iniciando containers..."
docker-compose -f docker-compose.prod.yml up -d

# Aguardar health check
echo "⏳ Aguardando API ficar saudável..."
sleep 10
curl -f http://localhost:8080/health || exit 1

echo "✅ Deploy concluído com sucesso!"
#!/bin/bash
# scripts/migrate.sh

echo "🔄 Executando migrações..."

# Conectar ao banco e executar migrações
docker exec api_db psql -U app -d api_db -f /migrations/001_initial.sql
docker exec api_db psql -U app -d api_db -f /migrations/002_seed.sql

echo "✅ Migrações concluídas!"

Instruções de deploy no README.md:

# Deploy

## Pré-requisitos
- Docker e Docker Compose instalados
- Acesso ao Docker Hub

## Passos para deploy

1. Clone o repositório:
```bash
git clone https://github.com/seu-usuario/api-rest-go.git
cd api-rest-go
  1. Configure as variáveis de ambiente:
cp .env.example .env
# Edite o arquivo .env com suas configurações
  1. Execute o deploy:
chmod +x scripts/deploy.sh
./scripts/deploy.sh
  1. Verifique se a API está rodando:
curl http://localhost:8080/health

Variáveis de ambiente necessárias

Variável Descrição Exemplo
DB_HOST Host do banco de dados localhost
DB_PORT Porta do banco 5432
DB_USER Usuário do banco app
DB_PASSWORD Senha do banco app_secret
DB_NAME Nome do banco api_db
JWT_SECRET Chave secreta JWT sua_chave_secreta
SERVER_PORT Porta do servidor 8080
```

Conclusão

Neste projeto final, desenvolvemos uma API REST completa em Go com:

Autenticação JWT segura com bcrypt para hash de senhas
CRUD completo para usuários, produtos e pedidos
Validações de negócio como controle de estoque
Testes unitários e de integração com mocks e testcontainers
Dockerização com multi-stage build e Docker Compose
Documentação OpenAPI/Swagger automática
Scripts de deploy prontos para produção

A arquitetura modular permite fácil manutenção e escalabilidade, enquanto as práticas de segurança e testes garantem robustez para ambientes reais.

Próximos passos

  • Implementar rate limiting com Redis
  • Adicionar cache de consultas frequentes
  • Configurar CI/CD com GitHub Actions
  • Implementar monitoramento com Prometheus
  • Adicionar logs estruturados com zerolog

O código completo está disponível no repositório do projeto. Contribuições são bem-vindas!