Manejo de requests HTTP: parámetros, queries y cuerpos JSON
En esta lección, nos adentraremos en el núcleo de la construcción de APIs REST con Go y gorilla/mux: la extracción y el procesamiento de datos provenientes de las solicitudes HTTP. Dominar el manejo de parámetros de ruta, consultas de URL (queries) y cuerpos JSON es fundamental para crear endpoints flexibles, seguros y bien diseñados. Aprenderemos no solo a obtener estos datos, sino también a validarlos y estructurarlos de manera eficiente, aplicando las mejores prácticas de la industria para microservicios de alto rendimiento.
Concepto Clave: La Anatomía de una Solicitud HTTP
Imagina que tu API es un restaurante de alta cocina. Una solicitud HTTP (request) es el pedido que hace un cliente. Este pedido no llega como un grito desordenado, sino en un formato estructurado con partes específicas. La URL es como la dirección del restaurante y el nombre del plato específico (el recurso). Los parámetros de ruta son detalles incrustados en ese nombre, como "hamburguesa-doble-con-queso", donde "doble" y "con-queso" son parámetros que modifican el recurso base "hamburguesa".
Los parámetros de consulta (query parameters) son las instrucciones adicionales, como "?papas=fritas&bebida=cola". Son opcionales y no cambian el recurso fundamental que se está solicitando, sino que filtran, ordenan o personalizan la respuesta. Finalmente, el cuerpo (body) de la solicitud, especialmente en métodos como POST o PUT, es como la nota especial del cliente con ingredientes personalizados o instrucciones detalladas para la cocina, típicamente estructurada en formato JSON. Gorilla/mux actúa como el maître experto que sabe exactamente dónde buscar cada parte de esta información y te la presenta de manera ordenada para que tú, el "chef" (tu lógica de negocio), puedas procesarla.
Cómo funciona en la práctica: Desensamblando un Request con gorilla/mux
En la práctica, cuando un request llega a tu servidor Go, el enrutador gorilla/mux lo intercepta basándose en el patrón de URL y el método HTTP que definiste. Tu función manejadora (handler) recibe un objeto http.ResponseWriter para construir la respuesta y un puntero a http.Request que contiene toda la información de la solicitud entrante. El trabajo consiste en inspeccionar este objeto `http.Request` para extraer los datos necesarios.
Para los parámetros de ruta (ej: `/usuarios/{id}`), gorilla/mux los parsea automáticamente y los almacena en un mapa. Puedes acceder a ellos usando `mux.Vars(r)`. Para los parámetros de consulta (ej: `?filtro=activo&orden=desc`), debes acceder al objeto `URL` del request y a su propiedad `Query()` que devuelve un mapa de tipo `url.Values`. El cuerpo JSON requiere un paso más: debes leer el `Body` del request (un `io.ReadCloser`), decodificarlo usando el paquete `encoding/json` en una estructura de Go (struct) predefinida que refleje la forma esperada de los datos. Es crucial cerrar el body y manejar posibles errores de decodificación en este proceso.
Código en Acción: Un Endpoint Completo para Gestión de Usuarios
Vamos a construir un endpoint práctico que combine los tres tipos de datos. Crearemos un endpoint `GET /api/usuarios/{id}` que acepte un parámetro de ruta (id), un query para los detalles extendidos (`?detalles=true`) y un endpoint `POST /api/usuarios` que procese un cuerpo JSON para crear un nuevo usuario.
package main
import (
"encoding/json"
"log"
"net/http"
"strconv"
"github.com/gorilla/mux"
)
// Estructuras de datos
type Usuario struct {
ID int `json:"id"`
Nombre string `json:"nombre"`
Email string `json:"email"`
Activo bool `json:"activo"`
}
type CrearUsuarioRequest struct {
Nombre string `json:"nombre"`
Email string `json:"email"`
}
// Handler para GET /api/usuarios/{id}
func ObtenerUsuarioHandler(w http.ResponseWriter, r *http.Request) {
// 1. Obtener parámetro de ruta
vars := mux.Vars(r)
idStr := vars["id"]
id, err := strconv.Atoi(idStr)
if err != nil {
http.Error(w, "ID de usuario inválido", http.StatusBadRequest)
return
}
// 2. Obtener parámetros de consulta (queries)
query := r.URL.Query()
detalles := query.Get("detalles") // Retorna un string vacío si no existe
incluirDetalles := detalles == "true"
// 3. Lógica de negocio (simulada)
usuario := Usuario{ID: id, Nombre: "Juan Pérez", Email: "[email protected]", Activo: true}
// 4. Respuesta condicional basada en query
var respuesta interface{} = usuario
if incluirDetalles {
tipoUsuario := "Estándar"
if id > 100 {
tipoUsuario = "Premium"
}
respuesta = struct {
Usuario
Tipo string `json:"tipo"`
DetalleHora string `json:"detalle_hora,omitempty"`
}{
Usuario: usuario,
Tipo: tipoUsuario,
}
}
// 5. Codificar y enviar respuesta JSON
w.Header().Set("Content-Type", "application/json")
if err := json.NewEncoder(w).Encode(respuesta); err != nil {
http.Error(w, "Error al codificar respuesta", http.StatusInternalServerError)
}
}
// Handler para POST /api/usuarios
func CrearUsuarioHandler(w http.ResponseWriter, r *http.Request) {
// 1. Validar método y contenido
if r.Header.Get("Content-Type") != "application/json" {
http.Error(w, "Content-Type debe ser application/json", http.StatusUnsupportedMediaType)
return
}
// 2. Decodificar el cuerpo JSON
var req CrearUsuarioRequest
decoder := json.NewDecoder(r.Body)
decoder.DisallowUnknownFields() // Importante: rechaza campos no esperados
err := decoder.Decode(&req)
if err != nil {
http.Error(w, "Cuerpo JSON inválido: "+err.Error(), http.StatusBadRequest)
return
}
// El body se cierra automáticamente por el servidor en la mayoría de los casos, pero es buena práctica recordarlo.
// 3. Validaciones básicas de negocio
if req.Nombre == "" || req.Email == "" {
http.Error(w, "Nombre y email son campos obligatorios", http.StatusBadRequest)
return
}
// 4. Simular creación y respuesta
nuevoUsuario := Usuario{
ID: 999, // ID simulado, normalmente lo genera la base de datos
Nombre: req.Nombre,
Email: req.Email,
Activo: true,
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusCreated) // Código 201 para recurso creado
if err := json.NewEncoder(w).Encode(nuevoUsuario); err != nil {
http.Error(w, "Error al codificar respuesta", http.StatusInternalServerError)
}
}
func main() {
r := mux.NewRouter()
// Definición de rutas
api := r.PathPrefix("/api").Subrouter()
api.HandleFunc("/usuarios/{id:[0-9]+}", ObtenerUsuarioHandler).Methods("GET")
api.HandleFunc("/usuarios", CrearUsuarioHandler).Methods("POST")
log.Println("Servidor iniciado en http://localhost:8080")
log.Fatal(http.ListenAndServe(":8080", r))
}
Errores Comunes y Cómo Evitarlos
Al manejar datos de entrada, los errores son frecuentes. Aquí detallamos los más comunes y sus soluciones:
1. No Validar Parámetros de Ruta: Asumir que el `id` extraído con `mux.Vars(r)` es siempre un número válido es un error grave. Siempre debes convertirlo y manejar el error (ej: con `strconv.Atoi`). Un valor malintencionado podría causar un panic o inyección de lógica en tu base de datos.
2. Ignorar la Codificación de Caracteres en Queries: Los parámetros de consulta vienen codificados en URL (ej: los espacios son `%20`). El paquete `net/url` de Go los decodifica automáticamente al usar `r.URL.Query()`. El error está en no recordar que los valores ya están decodificados, o en intentar decodificarlos nuevamente, lo que puede corromper los datos.
3. Leer el Cuerpo (Body) del Request Múltiples Veces: El `Body` de `http.Request` es un `io.ReadCloser` que solo puede leerse una vez. Si intentas leerlo en un middleware y luego en tu handler, el segundo intento encontrará un stream vacío. La solución es leerlo una vez, almacenar los bytes o el objeto decodificado en el contexto del request, o usar `r.GetBody()` (no siempre disponible).
4. No Limitar el Tamaño del Cuerpo del Request: Aceptar cuerpos JSON de tamaño ilimitado es una puerta abierta a ataques de denegación de servicio (DoS). Debes configurar límites usando `http.MaxBytesReader` o a nivel de servidor. Esto protege tu aplicación de clientes que envían datos masivos para consumir memoria y CPU.
Tip de Seguridad: Siempre usa `decoder.DisallowUnknownFields()` al decodificar JSON. Esto previene que un cliente envíe campos adicionales no definidos en tu struct, lo que podría ser un intento de explotar lógica no manejada o causar confusiones en el mapeo de datos.
Checklist de Dominio
Para asegurar que has comprendido y puedes implementar correctamente el manejo de requests HTTP, verifica que puedes realizar las siguientes tareas:
- Definir una ruta en gorilla/mux que capture un parámetro numérico y otro de tipo string (ej: `/productos/{categoria}/{id:\\d+}`).
- Escribir un handler que extraiga y valide ambos parámetros de ruta, devolviendo un error HTTP 400 si el ID no es un número.
- Construir una URL de prueba con múltiples parámetros de consulta, incluidos uno repetido (ej: `?tag=go&tag=api&sort=asc`), y escribir código para leer todos los valores del parámetro `tag` como un slice.
- Definir una estructura de Go con etiquetas JSON para anidamiento (`json:"direccion.calle"` no es válido en Go, debes usar structs anidados) y campos omitempty, y decodificar exitosamente un JSON que la represente.
- Implementar un middleware básico que limite el tamaño del cuerpo de todas las solicitudes POST a 1MB y responda con `http.StatusRequestEntityTooLarge (413)` si se excede.
- Diferenciar claramente cuándo usar `http.StatusBadRequest (400)` (error del cliente en los datos) y `http.StatusInternalServerError (500)` (error inesperado en el servidor) al manejar errores en los handlers.
- Configurar correctamente el header `Content-Type: application/json` tanto en las respuestas exitosas como en los mensajes de error que devuelvan JSON.
- Manejar el cierre del `Body` del request de manera explícita o justificar por qué no es necesario en tu flujo específico (ej: el servidor HTTP lo cierra).
Dominar estos conceptos transformará tu forma de construir APIs. Dejarás de simplemente "recibir datos" para "orquestar conversaciones" estructuradas, seguras y eficientes entre tu microservicio y sus clientes. Cada request se convierte en una oportunidad para aplicar validación, seguridad y claridad en el diseño, pilares de un sistema de alto rendimiento y mantenible.