Saltar al contenido

Lección 4 de 25 · Lectura y práctica

Práctica: Crear una API simple para gestión de usuarios

Introducción: Construyendo Nuestro Primer Microservicio En esta lección práctica, consolidaremos los fundamentos del módulo construyendo una API REST funcional para la gestión de usuarios. Este es el primer bloque de construcción.

Del concepto a la práctica

Taller de esta lección

Una API de usuarios resulta más útil si cada operación tiene una precondición comprobable. Usa una colección en memoria con ID, Name y Email; no permitas dos correos iguales. El flujo trabajado crea Ana, consulta su ID, actualiza su nombre y finalmente elimina el registro.

Ejemplo resuelto

  1. Implementa POST /users y rechaza nombre o correo vacíos con 400.

  2. Si el correo ya existe, responde 409 sin insertar.

  3. Implementa GET /users/{id} y devuelve 404 cuando no existe.

  4. Usa PUT para reemplazo completo y DELETE con 204 tras borrar.

Pruébalo

Crea Ana, prueba inmediatamente el POST duplicado, consulta y actualiza el registro, y elimínalo al final.

Comprueba tu resultado

Debe observarse: POST 201 con ID 1, POST duplicado 409, GET 200, PUT 200 con el nuevo nombre y DELETE 204. La lista final queda vacía.

Error frecuente: confundir idempotencia con repetir el mismo status. En este contrato, borrar por segunda vez devuelve 404; otros contratos pueden devolver 204. Ambos pueden ser idempotentes si no crean efectos adicionales. Documenta tu decisión y no escribas body con 204.

Navegador, API y base de datos conectados por solicitudes, consultas y respuestas en sentidos opuestos.
CursaloMapa de práctica
Del navegador a los datos

El navegador envía una solicitud a la API. El servidor valida la operación y consulta los datos; después devuelve una respuesta que el navegador puede mostrar. La base de datos se comunica con el servidor y no directamente con el navegador.

Ver la infografía en tamaño completo ↗
Fuentes para profundizar

Introducción: Construyendo Nuestro Primer Microservicio

En esta lección práctica, consolidaremos los fundamentos del módulo construyendo una API REST funcional para la gestión de usuarios. Este es el primer bloque de construcción tangible hacia un microservicio en Go. Nos alejaremos de la teoría abstracta para sumergirnos en el código, definiendo rutas, manejadores, estructuras de datos y la lógica de negocio básica que forma el núcleo de cualquier servicio backend. Utilizaremos Gorilla/Mux como nuestro enrutador por su flexibilidad y potencia, y modelaremos nuestros datos en memoria para mantener la simplicidad y el foco en los mecanismos HTTP y la arquitectura de la API.

El objetivo final es crear un servicio con operaciones CRUD completas (Crear, Leer, Actualizar, Eliminar) sobre un recurso de "Usuario". Este patrón es universal y sentará las bases para entender cómo estructurar endpoints, manejar parámetros de ruta y consulta, procesar cuerpos de solicitud JSON y devolver respuestas HTTP apropiadas con códigos de estado. Al final de esta lección, tendrás un servidor en ejecución que puede gestionar una colección de usuarios, un logro fundamental en tu camino hacia el desarrollo de APIs de alto rendimiento con Go.

Concepto Clave: El Enrutador como Portero y Guía

Imagina un gran edificio (tu servidor de API) con muchas habitaciones (endpoints o funcionalidades). Un cliente llega desde la calle (hace una solicitud HTTP) y necesita llegar a una habitación específica, como "Registrar un nuevo usuario" o "Consultar el perfil del usuario 5". El enrutador (en nuestro caso, Gorilla/Mux) es el portero y guía inteligente de este edificio. Su trabajo es interceptar a cada cliente que llega, mirar su intención (el método HTTP: GET, POST, etc.) y la dirección que pide (la URL o ruta), y decidir exactamente a qué habitación (función manejadora) debe dirigirlo para que sea atendido.

Sin este portero, los clientes estarían perdidos, golpeando puertas al azar o yendo a lugares incorrectos, resultando en errores 404 (Not Found) o un comportamiento caótico. Gorilla/Mux no solo dirige el tráfico, sino que también puede verificar las credenciales del cliente (middleware de autenticación), entender direcciones complejas con variables (como /users/{id}), y asegurarse de que el cliente solo entre a las habitaciones permitidas para él. Es el componente central que orquesta el flujo de todas las solicitudes en tu API, haciendo posible que una sola aplicación escuche en un puerto y maneje docenas de endpoints distintos de manera organizada y eficiente.

Cómo Funciona en la Práctica: Flujo de una Solicitud

Vamos a desglosar el viaje paso a paso de una solicitud para crear un usuario (POST /users). Primero, el cliente (como curl o Postman) envía una solicitud HTTP POST a la URL http://localhost:8080/users. El cuerpo de esta solicitud contiene un objeto JSON con los datos del nuevo usuario, como nombre y email. Nuestro servidor Go, que está ejecutándose y escuchando en el puerto 8080, recibe esta solicitud cruda. Inmediatamente, el enrutador Gorilla/Mux, que hemos configurado previamente, entra en acción.

El enrutador compara la ruta solicitada (/users) y el método (POST) con la lista de rutas que le hemos registrado. Al encontrar una coincidencia, transfiere el control al manejador de funciones específico que nosotros escribimos para esa ruta, por ejemplo, CreateUserHandler. Esta función es donde reside nuestra lógica de negocio. Aquí, decodificamos el cuerpo JSON de la solicitud en una estructura de datos Go (un struct User), validamos la información, generamos un ID único, almacenamos el usuario en nuestro "almacén" en memoria (un mapa o slice) y finalmente, construimos y enviamos una respuesta HTTP de vuelta al cliente, típicamente con un código de estado 201 (Created) y el JSON del usuario recién creado en el cuerpo.

Tip: Cada manejador (handler) debe ser independiente y enfocado en una sola tarea. Su responsabilidad es procesar la solicitud para su ruta específica y generar la respuesta adecuada. Mantenerlos pequeños y enfocados mejora la legibilidad, la capacidad de prueba y el mantenimiento del código.

Código en Acción: La Estructura Base y el Primer Endpoint

Comencemos definiendo las estructuras de datos y la configuración inicial del servidor. Crearemos un tipo User para modelar nuestros datos y usaremos un mapa en memoria, protegido por un mutex para seguridad en concurrencia, como almacén temporal. Luego, inicializaremos el enrutador Gorilla/Mux y definiremos nuestro primer endpoint: la ruta para obtener la lista de todos los usuarios (GET /users). Este es un punto de partida claro que nos permite ver la conexión entre ruta, manejador y respuesta.

package main

import (
    "encoding/json"
    "log"
    "net/http"
    "sync"
    "github.com/gorilla/mux"
)

// User define la estructura de nuestro recurso.
type User struct {
    ID    string `json:"id"`
    Name  string `json:"name"`
    Email string `json:"email"`
}

// Almacén en memoria con un mutex para seguridad en concurrencia.
var (
    users   = make(map[string]User)
    usersMu sync.RWMutex // Mutex de Lectura/Escritura
)

func main() {
    // Inicializar el enrutador Gorilla/Mux.
    r := mux.NewRouter()

    // Definir las rutas y asociarlas a sus manejadores.
    r.HandleFunc("/users", GetUsersHandler).Methods("GET")
    // NOTA: Aquí registraremos POST, GET por ID, PUT y DELETE más adelante.

    // Configurar el servidor HTTP.
    log.Println("Servidor API de usuarios iniciado en http://localhost:8080")
    log.Fatal(http.ListenAndServe(":8080", r))
}

// GetUsersHandler maneja las solicitudes GET a /users.
func GetUsersHandler(w http.ResponseWriter, r *http.Request) {
    // Adquirir el candado para lectura.
    usersMu.RLock()
    defer usersMu.RUnlock()

    // Convertir el mapa de usuarios a un slice para la respuesta.
    userList := make([]User, 0, len(users))
    for _, user := range users {
        userList = append(userList, user)
    }

    // Establecer el encabezado Content-Type a application/json.
    w.Header().Set("Content-Type", "application/json")
    // Codificar el slice de usuarios a JSON y escribirlo en el ResponseWriter.
    json.NewEncoder(w).Encode(userList)
}

Este código establece la base. El manejador GetUsersHandler adquiere un candado de lectura (RLock()) para acceder de forma segura al mapa users desde múltiples goroutines (peticiones concurrentes). Luego, itera el mapa, construye un slice, establece el encabezado de respuesta correcto y codifica los datos a JSON. Ejecuta este programa y visita http://localhost:8080/users en tu navegador o con curl; deberías ver una respuesta JSON vacía [], lo que es correcto ya que no hay usuarios aún. ¡Tu API ya está viva y respondiendo!

Completando el CRUD: Crear, Obtener, Actualizar y Eliminar

Con el servidor base funcionando, es hora de implementar las operaciones restantes para tener un CRUD completo. Necesitamos manejar la creación de usuarios (POST), la obtención de uno específico (GET por ID), la actualización (PUT) y la eliminación (DELETE). Cada una de estas operaciones presenta sus propios matices: POST debe leer el cuerpo de la solicitud, PUT debe reemplazar un recurso existente, y DELETE debe eliminar. Además, los endpoints que operan sobre un recurso específico utilizarán parámetros de ruta de Gorilla/Mux, como {id}, para identificar al usuario.

La gestión de errores se vuelve crucial aquí. Debemos responder con códigos de estado HTTP apropiados: 201 Created para creación exitosa, 200 OK para éxito general, 404 Not Found cuando un usuario no existe, y 400 Bad Request para datos de entrada inválidos. También debemos asegurarnos de que nuestras operaciones de escritura en el mapa users estén protegidas por el mutex de escritura (Lock()) para evitar condiciones de carrera. A continuación, expandimos nuestra función main y añadimos los nuevos manejadores.

// --- Dentro de la función main(), después de la ruta GET /users ---
r.HandleFunc("/users", CreateUserHandler).Methods("POST")
r.HandleFunc("/users/{id}", GetUserHandler).Methods("GET")
r.HandleFunc("/users/{id}", UpdateUserHandler).Methods("PUT")
r.HandleFunc("/users/{id}", DeleteUserHandler).Methods("DELETE")

// CreateUserHandler maneja las solicitudes POST a /users.
func CreateUserHandler(w http.ResponseWriter, r *http.Request) {
    var newUser User

    // Intentar decodificar el cuerpo JSON de la solicitud.
    if err := json.NewDecoder(r.Body).Decode(&newUser); err != nil {
        http.Error(w, "Datos JSON inválidos", http.StatusBadRequest)
        return
    }

    // Validación simple: nombre y email no vacíos.
    if newUser.Name == "" || newUser.Email == "" {
        http.Error(w, "Los campos 'name' y 'email' son obligatorios", http.StatusBadRequest)
        return
    }

    // Generar un ID simple (en producción, usaría un UUID).
    newUser.ID = "user_" + string(len(users)+1)

    // Adquirir candado para escritura y almacenar el usuario.
    usersMu.Lock()
    users[newUser.ID] = newUser
    usersMu.Unlock()

    // Responder con el usuario creado y código 201.
    w.Header().Set("Content-Type", "application/json")
    w.WriteHeader(http.StatusCreated)
    json.NewEncoder(w).Encode(newUser)
}

// GetUserHandler maneja las solicitudes GET a /users/{id}.
func GetUserHandler(w http.ResponseWriter, r *http.Request) {
    // Extraer el parámetro de ruta "id" usando Gorilla/Mux.
    vars := mux.Vars(r)
    id := vars["id"]

    // Adquirir candado para lectura.
    usersMu.RLock()
    user, exists := users[id]
    usersMu.RUnlock()

    if !exists {
        http.Error(w, "Usuario no encontrado", http.StatusNotFound)
        return
    }

    w.Header().Set("Content-Type", "application/json")
    json.NewEncoder(w).Encode(user)
}

// UpdateUserHandler maneja las solicitudes PUT a /users/{id}.
func UpdateUserHandler(w http.ResponseWriter, r *http.Request) {
    vars := mux.Vars(r)
    id := vars["id"]

    usersMu.Lock()
    defer usersMu.Unlock()

    // Verificar que el usuario a actualizar exista.
    if _, exists := users[id]; !exists {
        http.Error(w, "Usuario no encontrado", http.StatusNotFound)
        return
    }

    var updatedData User
    if err := json.NewDecoder(r.Body).Decode(&updatedData); err != nil {
        http.Error(w, "Datos JSON inválidos", http.StatusBadRequest)
        return
    }

    // Preservar el ID de la ruta y actualizar los campos permitidos.
    updatedData.ID = id
    // Validación básica.
    if updatedData.Name == "" || updatedData.Email == "" {
        http.Error(w, "Los campos 'name' y 'email' son obligatorios", http.StatusBadRequest)
        return
    }

    users[id] = updatedData

    w.Header().Set("Content-Type", "application/json")
    json.NewEncoder(w).Encode(updatedData)
}

// DeleteUserHandler maneja las solicitudes DELETE a /users/{id}.
func DeleteUserHandler(w http.ResponseWriter, r *http.Request) {
    vars := mux.Vars(r)
    id := vars["id"]

    usersMu.Lock()
    defer usersMu.Unlock()

    if _, exists := users[id]; !exists {
        http.Error(w, "Usuario no encontrado", http.StatusNotFound)
        return
    }

    delete(users, id)
    w.WriteHeader(http.StatusNoContent) // 204 No Content es apropiado para DELETE exitoso.
}

Errores Comunes y Cómo Evitarlos

Al construir esta primera API, varios errores frecuentes pueden surgir y comprometer la funcionalidad, seguridad o rendimiento. El primero es no manejar la concurrencia. Go maneja cada solicitud HTTP en una goroutine separada. Acceder al mapa users sin sincronización (con sync.RWMutex) desde múltiples goroutines causará pánicos o corrupción de datos. Siemque protejas las estructuras de datos compartidas.

El segundo error es olvidar establecer los encabezados HTTP, específicamente Content-Type: application/json. Sin él, los clientes pueden no interpretar correctamente la respuesta. El tercero es devolver códigos de estado HTTP incorrectos. Usar siempre 200 OK, incluso para errores, confunde a los clientes. Aprende y usa los códigos apropiados: 400 para errores del cliente, 404 para recursos no encontrados, 201 para creación exitosa, 204 para eliminación exitosa sin cuerpo.

Un cuarto error sutil es no validar ni sanitizar la entrada del usuario. En nuestro ejemplo, hacemos una validación mínima. En una API real, debes validar rigurosamente todos los campos (formato de email, longitud del nombre, etc.) para prevenir datos erróneos o ataques de inyección. Finalmente, no cerrar el cuerpo de la solicitud (r.Body.Close()) puede ser un problema en manejadores más complejos. Si lees el cuerpo completo con json.Decoder o ioutil.ReadAll, generalmente no es necesario cerrarlo explícitamente, pero es un detalle a recordar al trabajar con streams.

Tip de Depuración: Usa log.Printf para imprimir información clave en la consola del servidor, como el ID recibido en una ruta o los datos decodificados. Esto te ayuda a seguir el flujo de la solicitud y a identificar dónde falla la lógica. Recuerda eliminarlos o convertirlos en logs estructurados para producción.

Checklist de Dominio

Antes de considerar esta lección completa, asegúrate de poder verificar cada uno de los siguientes puntos. Demuestran una comprensión práctica y aplicada de la construcción de una API REST simple con Go y Gorilla/Mux.

  • Puedo explicar el rol del enrutador (Gorilla/Mux) en el flujo de una solicitud HTTP.
  • He escrito y ejecutado un servidor Go que define múltiples rutas para un recurso (usuarios) usando mux.NewRouter() y HandleFunc().
  • He implementado manejadores funcionales para las cinco operaciones CRUD básicas (GET lista, GET item, POST, PUT, DELETE).
  • Puedo extraer y utilizar parámetros de ruta (como {id}) dentro de un manejador usando mux.Vars(r).
  • Sé cómo decodificar un cuerpo de solicitud JSON en una estructura Go y codificar una estructura Go en una respuesta JSON.
  • He utilizado sync.RWMutex para proteger el acceso concurrente a una estructura de datos en memoria compartida entre goroutines.
  • Puedo listar al menos tres códigos de estado HTTP diferentes (200, 201, 400, 404, 204) y sé en qué situación usar cada uno en mis respuestas de API.
  • He probado mi API completa usando una herramienta como curl o Postman, verificando que cada endpoint se comporta como se espera.

Sigue con APIs REST en Go: Concurrencia, Despliegue y Monitoreo

Esta lección forma parte de un curso de Cursalo. Revisa su programa y elige entre la compra individual o el acceso a toda la escuela.

Si ya tenías acceso a este curso, entra con tu cuenta: tus derechos y tu progreso se conservan. No necesitas comprarlo de nuevo.

01 / ELEGIR UNA RUTA

El próximo paso

Aprende IA mientras construyes algo que sí necesitas.

Empieza por el curso inicial o explora el catálogo para encontrar una práctica alineada con tu trabajo.