Este manual detalla cómo construir una API RESTful completa para una galería de imágenes con:
Requisitos previos:
Cliente → API REST (Go) → Base de Datos → Almacenamiento de Imágenes
│
↓
Autenticación (JWT)
│
↓
Documentación (Swagger UI)
mkdir gallery-api
cd gallery-api
go mod init github.com/tu-usuario/gallery-api
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@latest # Generador Swagger
go get github.com/swaggo/gin-swagger # Integración Swagger con Gin
go get gorm.io/gorm # ORM
go get gorm.io/driver/postgres # Driver PostgreSQL
go get github.com/google/uuid # Generación de UUIDs
go get github.com/disintegration/imaging # Procesamiento de imágenes
gallery-api/
├── cmd/
│ └── server/
│ └── main.go # Punto de entrada
├── internal/
│ ├── auth/ # Autenticación
│ │ ├── handler.go # Manejadores HTTP
│ │ ├── service.go # Lógica de negocio
│ │ └── repository.go # Acceso a datos
│ ├── gallery/ # Gestión de galería
│ ├── user/ # Gestión de usuarios
│ ├── config/ # Configuración
│ ├── middleware/ # Middlewares
│ └── models/ # Modelos de datos
├── pkg/
│ ├── database/ # Conexión a DB
│ ├── storage/ # Almacenamiento de imágenes
│ └── utils/ # Utilidades
├── docs/ # Documentación Swagger
├── uploads/ # Imágenes subidas (en producción usar S3 o similar)
├── .env # Variables de entorno
├── Makefile # Automatización
└── go.mod # Dependencias
// internal/models/user.go
package models
import (
"time"
"github.com/google/uuid"
"gorm.io/gorm"
)
type User struct {
gorm.Model
ID uuid.UUID `gorm:"type:uuid;primaryKey"`
Username string `gorm:"unique;not null"`
Email string `gorm:"unique;not null"`
Password string `gorm:"not null"`
Images []Image `gorm:"foreignKey:UserID"`
CreatedAt time.Time
UpdatedAt time.Time
}
func (u *User) BeforeCreate(tx *gorm.DB) error {
u.ID = uuid.New()
return nil
}
// internal/models/image.go
package models
import (
"time"
"github.com/google/uuid"
"gorm.io/gorm"
)
type Image struct {
gorm.Model
ID uuid.UUID `gorm:"type:uuid;primaryKey"`
UserID uuid.UUID `gorm:"type:uuid;not null"`
Title string `gorm:"not null"`
Description string
Filename string `gorm:"not null"`
Filepath string `gorm:"not null"`
Size int64 `gorm:"not null"`
MimeType string `gorm:"not null"`
CreatedAt time.Time
UpdatedAt time.Time
}
func (i *Image) BeforeCreate(tx *gorm.DB) error {
i.ID = uuid.New()
return nil
}
// internal/auth/jwt.go
package auth
import (
"time"
"github.com/golang-jwt/jwt/v4"
)
var jwtSecret = []byte("secret_key") // Debe ser una variable de entorno en producción
type Claims struct {
UserID uuid.UUID `json:"user_id"`
jwt.RegisteredClaims
}
func GenerateToken(userID uuid.UUID) (string, error) {
expirationTime := time.Now().Add(24 * time.Hour)
claims := &Claims{
UserID: userID,
RegisteredClaims: jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(expirationTime),
IssuedAt: jwt.NewNumericDate(time.Now()),
Issuer: "gallery-api",
},
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
return token.SignedString(jwtSecret)
}
func ParseToken(tokenString string) (*Claims, error) {
claims := &Claims{}
token, err := jwt.ParseWithClaims(tokenString, claims, func(token *jwt.Token) (interface{}, error) {
return jwtSecret, 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"
"github.com/google/uuid"
"github.com/tu-usuario/gallery-api/internal/models"
"golang.org/x/crypto/bcrypt"
)
type RegisterRequest struct {
Username string `json:"username" binding:"required,min=3,max=50"`
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.repo.GetUserByEmail(req.Email); err == nil {
c.JSON(http.StatusConflict, gin.H{"error": "email ya registrado"})
return
}
// Hashear contraseña
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost)
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: string(hashedPassword),
}
if err := h.repo.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"})
}
// internal/auth/handler.go
type LoginRequest struct {
Email string `json:"email" binding:"required,email"`
Password string `json:"password" binding:"required"`
}
type LoginResponse struct {
Token string `json:"token"`
}
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.repo.GetUserByEmail(req.Email)
if err != nil {
c.JSON(http.StatusUnauthorized, gin.H{"error": "credenciales inválidas"})
return
}
// Verificar contraseña
if err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(req.Password)); err != nil {
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, LoginResponse{Token: token})
}
// internal/gallery/handler.go
package gallery
import (
"net/http"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
"github.com/tu-usuario/gallery-api/internal/models"
"github.com/tu-usuario/gallery-api/pkg/storage"
"mime/multipart"
)
type UploadRequest struct {
Title string `form:"title" binding:"required"`
Description string `form:"description"`
File *multipart.FileHeader `form:"file" binding:"required"`
}
func (h *GalleryHandler) UploadImage(c *gin.Context) {
userID, exists := c.Get("userID")
if !exists {
c.JSON(http.StatusUnauthorized, gin.H{"error": "no autorizado"})
return
}
var req UploadRequest
if err := c.ShouldBind(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
// Validar tipo de archivo
if !storage.IsImage(req.File) {
c.JSON(http.StatusBadRequest, gin.H{"error": "solo se permiten imágenes"})
return
}
// Guardar archivo
filePath, err := storage.SaveUploadedFile(req.File)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "error al guardar imagen"})
return
}
// Crear registro en la base de datos
image := models.Image{
UserID: userID.(uuid.UUID),
Title: req.Title,
Description: req.Description,
Filename: req.File.Filename,
Filepath: filePath,
Size: req.File.Size,
MimeType: req.File.Header.Get("Content-Type"),
}
if err := h.repo.CreateImage(&image); err != nil {
storage.DeleteFile(filePath)
c.JSON(http.StatusInternalServerError, gin.H{"error": "error al guardar metadatos"})
return
}
c.JSON(http.StatusCreated, gin.H{"message": "imagen subida exitosamente", "image_id": image.ID})
}
// internal/gallery/handler.go
type ImageResponse struct {
ID uuid.UUID `json:"id"`
Title string `json:"title"`
Description string `json:"description"`
URL string `json:"url"`
CreatedAt time.Time `json:"created_at"`
}
func (h *GalleryHandler) GetImages(c *gin.Context) {
userID, exists := c.Get("userID")
if !exists {
c.JSON(http.StatusUnauthorized, gin.H{"error": "no autorizado"})
return
}
images, err := h.repo.GetImagesByUserID(userID.(uuid.UUID))
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "error al obtener imágenes"})
return
}
response := make([]ImageResponse, len(images))
for i, img := range images {
response[i] = ImageResponse{
ID: img.ID,
Title: img.Title,
Description: img.Description,
URL: storage.GetImageURL(img.Filepath),
CreatedAt: img.CreatedAt,
}
}
c.JSON(http.StatusOK, response)
}
// internal/middleware/auth.go
package middleware
import (
"github.com/gin-gonic/gin"
"github.com/tu-usuario/gallery-api/internal/auth"
"net/http"
"strings"
)
func JWTAuthMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
authHeader := c.GetHeader("Authorization")
if authHeader == "" {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "autorización requerida"})
return
}
tokenParts := strings.Split(authHeader, " ")
if len(tokenParts) != 2 || tokenParts[0] != "Bearer" {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "formato de token inválido"})
return
}
claims, err := auth.ParseToken(tokenParts[1])
if err != nil {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "token inválido"})
return
}
c.Set("userID", claims.UserID)
c.Next()
}
}
// internal/middleware/cors.go
package middleware
import "github.com/gin-gonic/gin"
func CORSMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
c.Writer.Header().Set("Access-Control-Allow-Origin", "*")
c.Writer.Header().Set("Access-Control-Allow-Credentials", "true")
c.Writer.Header().Set("Access-Control-Allow-Headers", "Content-Type, Content-Length, Authorization")
c.Writer.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
if c.Request.Method == "OPTIONS" {
c.AbortWithStatus(204)
return
}
c.Next()
}
}
// cmd/server/main.go
// @title API de Galería
// @version 1.0
// @description API para gestión de galería de imágenes con autenticación JWT
// @host localhost:8080
// @BasePath /api/v1
// @securityDefinitions.apikey BearerAuth
// @in header
// @name Authorization
// internal/auth/handler.go
// Register godoc
// @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 /auth/register [post]
func (h *AuthHandler) Register(c *gin.Context) {
// ... implementación
}
// Login godoc
// @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} LoginResponse
// @Failure 400 {object} map[string]string
// @Failure 401 {object} map[string]string
// @Failure 500 {object} map[string]string
// @Router /auth/login [post]
func (h *AuthHandler) Login(c *gin.Context) {
// ... implementación
}
# Generar documentación
swag init -g cmd/server/main.go
// cmd/server/main.go
import (
"github.com/swaggo/gin-swagger"
"github.com/swaggo/gin-swagger/swaggerFiles"
_ "github.com/tu-usuario/gallery-api/docs"
)
func main() {
r := gin.Default()
// Configurar Swagger
r.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler))
// ... resto de la configuración
}
Accede a la documentación en: http://localhost:8080/swagger/index.html
# Compilar para Linux
GOOS=linux GOARCH=amd64 go build -o gallery-api cmd/server/main.go
# Configurar variables de entorno
cp .env.production .env
# /etc/systemd/system/gallery-api.service
[Unit]
Description=Gallery API Service
After=network.target
[Service]
User=gallery
Group=gallery
WorkingDirectory=/opt/gallery-api
ExecStart=/opt/gallery-api/gallery-api
Restart=always
EnvironmentFile=/opt/gallery-api/.env
[Install]
WantedBy=multi-user.target
# /etc/nginx/sites-available/gallery-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 /uploads/ {
alias /opt/gallery-api/uploads/;
}
location /swagger/ {
proxy_pass http://localhost:8080/swagger/;
}
}
Hemos construido una API REST completa para una galería de imágenes con:
Mejoras posibles: