Práctica: Implementar CRUD completo con validación

Lectura
30 min~5 min lectura

Concepto clave

En el desarrollo de APIs REST, un CRUD completo (Create, Read, Update, Delete) es la base de cualquier microservicio que maneje datos. Piensa en una biblioteca digital: necesitas agregar nuevos libros (Create), consultar el catálogo (Read), actualizar información de ejemplares (Update) y retirar libros obsoletos (Delete). La validación actúa como el bibliotecario que verifica que cada libro tenga título, autor y ISBN válidos antes de ingresarlo al sistema.

En Go con gorilla/mux, implementar CRUD con validación robusta significa combinar el enrutamiento eficiente del paquete con estructuras de datos bien definidas y validación en tiempo real. Esto evita que datos corruptos o malformados lleguen a tu base de datos, similar a cómo un filtro de correo bloquea spam antes de que inunde tu bandeja de entrada.

Cómo funciona en la práctica

Vamos a construir un microservicio para gestionar productos en un e-commerce. Primero, definimos una estructura Product con campos como ID, nombre, precio y stock. Luego, creamos rutas HTTP para cada operación CRUD usando gorilla/mux:

  1. POST /products: Crea un nuevo producto con validación de campos obligatorios y rangos de precio.
  2. GET /products/{id}: Recupera un producto específico por su ID.
  3. PUT /products/{id}: Actualiza un producto existente, validando que los nuevos datos sean coherentes.
  4. DELETE /products/{id}: Elimina un producto, verificando que exista antes de proceder.

La validación se implementa usando el paquete go-playground/validator, que permite definir reglas como "required", "gt=0" para precios positivos, o "max=100" para límites de stock. Cada handler verifica estas reglas antes de procesar la solicitud, devolviendo errores HTTP 400 (Bad Request) si fallan.

Código en acción

Aquí tienes la estructura base y un ejemplo de handler con validación:

package main

import (
    "encoding/json"
    "net/http"
    "github.com/go-playground/validator/v10"
    "github.com/gorilla/mux"
)

type Product struct {
    ID    string  `json:"id" validate:"omitempty,uuid"`
    Name  string  `json:"name" validate:"required,min=3,max=100"`
    Price float64 `json:"price" validate:"required,gt=0"`
    Stock int     `json:"stock" validate:"gte=0"`
}

var validate = validator.New()

func createProductHandler(w http.ResponseWriter, r *http.Request) {
    var product Product
    if err := json.NewDecoder(r.Body).Decode(&product); err != nil {
        http.Error(w, "Invalid JSON", http.StatusBadRequest)
        return
    }
    
    if err := validate.Struct(product); err != nil {
        http.Error(w, err.Error(), http.StatusBadRequest)
        return
    }
    
    // Lógica para guardar en base de datos (ejemplo simplificado)
    product.ID = generateUUID()
    w.WriteHeader(http.StatusCreated)
    json.NewEncoder(w).Encode(product)
}

func main() {
    r := mux.NewRouter()
    r.HandleFunc("/products", createProductHandler).Methods("POST")
    // Agregar más handlers para GET, PUT, DELETE
    http.ListenAndServe(":8080", r)
}

Ahora, mejoremos el handler para incluir manejo de errores más detallado:

func createProductHandler(w http.ResponseWriter, r *http.Request) {
    var product Product
    if err := json.NewDecoder(r.Body).Decode(&product); err != nil {
        respondWithError(w, http.StatusBadRequest, "Invalid request payload")
        return
    }
    
    if err := validate.Struct(product); err != nil {
        respondWithError(w, http.StatusBadRequest, formatValidationError(err))
        return
    }
    
    // Simular guardado en DB
    product.ID = uuid.New().String()
    if err := saveProduct(product); err != nil {
        respondWithError(w, http.StatusInternalServerError, "Could not save product")
        return
    }
    
    respondWithJSON(w, http.StatusCreated, product)
}

func respondWithError(w http.ResponseWriter, code int, message string) {
    respondWithJSON(w, code, map[string]string{"error": message})
}

func respondWithJSON(w http.ResponseWriter, code int, payload interface{}) {
    w.Header().Set("Content-Type", "application/json")
    w.WriteHeader(code)
    json.NewEncoder(w).Encode(payload)
}

Errores comunes

  • Validación solo en el cliente: Confiar únicamente en validación frontend deja tu API vulnerable a ataques. Siempre valida en el servidor, como verificar firmas en documentos legales.
  • Falta de manejo de errores específicos: Devolver "error" genérico dificulta debugging. Usa códigos HTTP precisos (400, 404, 500) y mensajes claros.
  • Ignorar concurrencia en operaciones CRUD: En microservicios de alto rendimiento, múltiples requests pueden modificar el mismo dato. Usa transacciones o locks para evitar condiciones de carrera.
  • No limitar tamaños de request: Aceptar JSON muy grandes puede saturar memoria. Configura límites con http.MaxBytesReader.
  • Olvidar sanitización de datos: La validación no previene inyecciones SQL o XSS. Usa parámetros preparados en queries y escapa outputs HTML.

Checklist de dominio

  1. Definí estructuras Go con tags de validación para todos los campos requeridos.
  2. Implementé handlers para las cuatro operaciones CRUD (POST, GET, PUT, DELETE) usando gorilla/mux.
  3. Integré el validador go-playground/validator en cada endpoint que recibe datos.
  4. Probé la API con herramientas como curl o Postman, enviando datos válidos e inválidos.
  5. Agregué manejo de errores específicos con respuestas JSON estandarizadas.
  6. Configuré límites de tamaño de request para prevenir ataques de denegación de servicio.
  7. Documenté los endpoints con ejemplos de request/response para futuros desarrolladores.

Implementa un CRUD para gestión de usuarios con validación avanzada

Construye un microservicio en Go que gestione usuarios para una plataforma online. Sigue estos pasos:

  1. Crea una estructura User con campos: ID (string), Email (string), Password (string), Age (int), y Active (bool). Aplica validaciones: Email debe ser válido y único, Password mínimo 8 caracteres, Age entre 18 y 100, Active opcional (default true).
  2. Configura gorilla/mux con estos endpoints:
    • POST /users - Crea usuario, validando todos los campos.
    • GET /users/{id} - Retorna usuario por ID, manejando no encontrado (404).
    • PUT /users/{id} - Actualiza usuario, permitiendo actualizar solo Email, Age y Active (Password requiere endpoint separado).
    • DELETE /users/{id} - Elimina usuario cambiando Active a false (borrado lógico).
  3. Implementa una validación custom para verificar que el Email no esté duplicado (simula con un mapa en memoria).
  4. Agrega middleware para loguear cada request con método, ruta y status code.
  5. Prueba con al menos 5 requests diferentes, incluyendo casos de error.
Pistas
  • Usa validator.RegisterValidation para crear una validación custom de email único.
  • Para el borrado lógico, en el handler DELETE actualiza el campo Active en lugar de remover el dato.
  • Considera usar sync.RWMutex si manejas concurrencia en el mapa de usuarios.

Evalua tu comprension

Completa el quiz interactivo de arriba para ganar XP.