Manejo de requests HTTP: parámetros, queries y cuerpos JSON

Video
25 min~8 min lectura

Reproductor de video

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.

De lección a portfolio

Convertí esta lección en una habilidad visible para entrevistas.

Guardá el curso, completá los ejercicios y conectá esta habilidad con una ruta de empleo, data, IA, programación o marketing.

Newsletter Cursalo

Recibí rutas y cursos nuevos

Sumate para recibir recursos orientados a empleo y portfolio.

  • Rutas de empleo
  • Cursos prácticos
  • Portfolio y entrevistas

Sin spam. También podés entrar con tu cuenta para guardar progreso. Iniciá sesión