Este manual detalla cómo construir una API RESTful segura en Go que incluye:
Requisitos previos:
mkdir go-rest-api
cd go-rest-api
go mod init github.com/tu-usuario/go-rest-api
Edita el archivo go.mod
o ejecuta:
go get github.com/gin-gonic/gin # Framework web
go get golang.org/x/crypto/bcrypt # Para hashing de contraseñas
go get github.com/golang-jwt/jwt/v4 # Para JWT
go get github.com/swaggo/swag/cmd/swag # Generador Swagger
go get github.com/swaggo/gin-swagger # Integración Swagger con Gin
go install github.com/swaggo/swag/cmd/swag@latest
go-rest-api/
├── cmd/
│ └── server/
│ └── main.go # Punto de entrada
├── internal/
│ ├── auth/ # Lógica de autenticación
│ │ ├── handler.go # Manejadores de auth
│ │ ├── service.go # Lógica de negocio
│ │ └── repository.go # Acceso a datos
│ ├── user/ # Gestión de usuarios
│ ├── config/ # Configuración
│ ├── middleware/ # Middlewares
│ └── models/ # Modelos de datos
├── pkg/
│ ├── database/ # Conexión a DB
│ └── utils/ # Utilidades
├── docs/ # Documentación Swagger
├── .env # Variables de entorno
└── Makefile # Automatización
// internal/models/user.go
package models
import "time"
type User struct {
ID uint `json:"id" gorm:"primaryKey"`
Username string `json:"username" gorm:"unique;not null"`
Email string `json:"email" gorm:"unique;not null"`
Password string `json:"-" gorm:"not null"` // No se serializa
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
// internal/auth/service.go
package auth
import "golang.org/x/crypto/bcrypt"
func HashPassword(password string) (string, error) {
bytes, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
return string(bytes), err
}
func CheckPasswordHash(password, hash string) bool {
err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password))
return err == nil
}
// internal/auth/service.go
import (
"time"
"github.com/golang-jwt/jwt/v4"
)
var jwtKey = []byte("tu_super_secreto_jwt") // Debería estar en variables de entorno
type Claims struct {
UserID uint `json:"user_id"`
jwt.StandardClaims
}
func GenerateToken(userID uint) (string, error) {
expirationTime := time.Now().Add(24 * time.Hour)
claims := &Claims{
UserID: userID,
StandardClaims: jwt.StandardClaims{
ExpiresAt: expirationTime.Unix(),
Issuer: "go-rest-api",
},
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
return token.SignedString(jwtKey)
}
func ValidateToken(tokenString string) (*Claims, error) {
claims := &Claims{}
token, err := jwt.ParseWithClaims(tokenString, claims, func(token *jwt.Token) (interface{}, error) {
return jwtKey, nil
})
if err != nil {
return nil, err
}
if !token.Valid {
return nil, jwt.ErrSignatureInvalid
}
return claims, nil
}
// internal/auth/handler.go
package auth
import (
"net/http"
"github.com/gin-gonic/gin"
)
type AuthHandler struct {
service AuthService
}
func NewAuthHandler(service AuthService) *AuthHandler {
return &AuthHandler{service: service}
}
type RegisterRequest struct {
Username string `json:"username" binding:"required"`
Email string `json:"email" binding:"required,email"`
Password string `json:"password" binding:"required,min=8"`
}
func (h *AuthHandler) Register(c *gin.Context) {
var req RegisterRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
// Verificar si el usuario ya existe
if _, err := h.service.GetUserByEmail(req.Email); err == nil {
c.JSON(http.StatusConflict, gin.H{"error": "email ya registrado"})
return
}
// Hashear contraseña
hashedPassword, err := HashPassword(req.Password)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "error al hashear contraseña"})
return
}
// Crear usuario
user := models.User{
Username: req.Username,
Email: req.Email,
Password: hashedPassword,
}
if err := h.service.CreateUser(&user); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "error al crear usuario"})
return
}
c.JSON(http.StatusCreated, gin.H{"message": "usuario creado exitosamente"})
}
type LoginRequest struct {
Email string `json:"email" binding:"required,email"`
Password string `json:"password" binding:"required"`
}
func (h *AuthHandler) Login(c *gin.Context) {
var req LoginRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
// Obtener usuario
user, err := h.service.GetUserByEmail(req.Email)
if err != nil {
c.JSON(http.StatusUnauthorized, gin.H{"error": "credenciales inválidas"})
return
}
// Verificar contraseña
if !CheckPasswordHash(req.Password, user.Password) {
c.JSON(http.StatusUnauthorized, gin.H{"error": "credenciales inválidas"})
return
}
// Generar token JWT
token, err := GenerateToken(user.ID)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "error al generar token"})
return
}
c.JSON(http.StatusOK, gin.H{"token": token})
}
// cmd/server/main.go
package main
import (
"github.com/gin-gonic/gin"
"github.com/tu-usuario/go-rest-api/internal/auth"
"github.com/tu-usuario/go-rest-api/internal/middleware"
"github.com/tu-usuario/go-rest-api/pkg/database"
"gorm.io/gorm"
)
var db *gorm.DB
func main() {
// Configuración inicial
r := gin.Default()
// Conexión a la base de datos
db = database.ConnectDB()
// Inicializar servicios
authRepo := auth.NewAuthRepository(db)
authService := auth.NewAuthService(authRepo)
authHandler := auth.NewAuthHandler(authService)
// Rutas públicas
public := r.Group("/api/v1")
{
public.POST("/register", authHandler.Register)
public.POST("/login", authHandler.Login)
}
// Rutas protegidas
protected := r.Group("/api/v1")
protected.Use(middleware.JWTAuthMiddleware())
{
protected.GET("/profile", func(c *gin.Context) {
userID := c.GetUint("userID")
// Lógica para obtener perfil...
})
}
// Iniciar servidor
r.Run(":8080")
}
// internal/middleware/auth.go
package middleware
import (
"github.com/gin-gonic/gin"
"github.com/golang-jwt/jwt/v4"
"github.com/tu-usuario/go-rest-api/internal/auth"
"net/http"
"strings"
)
func JWTAuthMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
// Obtener token del header Authorization
authHeader := c.GetHeader("Authorization")
if authHeader == "" {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "autorización requerida"})
return
}
// Formato esperado: "Bearer "
tokenParts := strings.Split(authHeader, " ")
if len(tokenParts) != 2 || tokenParts[0] != "Bearer" {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "formato de token inválido"})
return
}
tokenString := tokenParts[1]
// Validar token
claims, err := auth.ValidateToken(tokenString)
if err != nil {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "token inválido"})
return
}
// Añadir userID al contexto
c.Set("userID", claims.UserID)
c.Next()
}
}
// internal/auth/handler.go
// @Summary Registrar nuevo usuario
// @Description Crea una nueva cuenta de usuario
// @Tags auth
// @Accept json
// @Produce json
// @Param input body RegisterRequest true "Datos de registro"
// @Success 201 {object} map[string]string
// @Failure 400 {object} map[string]string
// @Failure 409 {object} map[string]string
// @Failure 500 {object} map[string]string
// @Router /api/v1/register [post]
func (h *AuthHandler) Register(c *gin.Context) {
// ... implementación existente
}
// @Summary Iniciar sesión
// @Description Autentica un usuario y devuelve un token JWT
// @Tags auth
// @Accept json
// @Produce json
// @Param input body LoginRequest true "Credenciales"
// @Success 200 {object} map[string]string
// @Failure 400 {object} map[string]string
// @Failure 401 {object} map[string]string
// @Failure 500 {object} map[string]string
// @Router /api/v1/login [post]
func (h *AuthHandler) Login(c *gin.Context) {
// ... implementación existente
}
# Generar archivos de documentación
swag init -g cmd/server/main.go
# Esto creará una carpeta docs con los archivos de Swagger
// cmd/server/main.go
import (
"github.com/swaggo/gin-swagger"
"github.com/swaggo/gin-swagger/swaggerFiles"
_ "github.com/tu-usuario/go-rest-api/docs" // Importar docs generados
)
func main() {
r := gin.Default()
// Configurar ruta Swagger
r.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler))
// ... resto de la configuración
}
Después de iniciar el servidor, puedes acceder a la documentación en:
http://localhost:8080/swagger/index.html
# Compilar para Linux desde cualquier sistema
GOOS=linux GOARCH=amd64 go build -o api cmd/server/main.go
# O compilar directamente en Linux
go build -o api cmd/server/main.go
# /etc/systemd/system/go-api.service
[Unit]
Description=Go REST API
After=network.target
[Service]
User=apiuser
Group=apiuser
WorkingDirectory=/opt/go-api
ExecStart=/opt/go-api/api
Restart=always
Environment="GIN_MODE=release"
Environment="DATABASE_URL=postgres://user:pass@localhost:5432/dbname"
[Install]
WantedBy=multi-user.target
# Recargar configuración
sudo systemctl daemon-reload
# Iniciar servicio
sudo systemctl start go-api
# Habilitar inicio automático
sudo systemctl enable go-api
# Ver estado
sudo systemctl status go-api
# /etc/nginx/sites-available/go-api
server {
listen 80;
server_name api.tudominio.com;
location / {
proxy_pass http://localhost:8080;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
location /swagger/ {
proxy_pass http://localhost:8080/swagger/;
}
}
Este manual ha cubierto todos los aspectos esenciales para construir una API REST segura en Go:
Próximos pasos recomendados:
Con esta base, puedes expandir tu API añadiendo más funcionalidades según tus necesidades específicas.