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
- Configure as variáveis de ambiente:
cp .env.example .env
# Edite o arquivo .env com suas configurações
- Execute o deploy:
chmod +x scripts/deploy.sh
./scripts/deploy.sh
- 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!