Saltar al contenido

Lección 3 de 26 · Lectura y práctica

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

Tipo de lección: Video Manejo de requests HTTP: parámetros, queries y cuerpos JSON 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.

Del concepto a la práctica

Taller de esta lección

Los tres lugares de entrada cumplen funciones distintas. En una búsqueda de libros, el parámetro /books/{id} identifica un recurso, ?author=Ríos&limit=2 filtra la colección y el cuerpo JSON describe una creación. Separarlos evita aceptar datos en el sitio equivocado.

Ejemplo resuelto

go
type CreateBook struct { Title string `json:"title"`; Pages int `json:"pages"` }
// id := mux.Vars(r)["id"]
// author := r.URL.Query().Get("author")
// err := json.NewDecoder(r.Body).Decode(&input)

Antes de usar Pages, exige que sea positivo y rechaza JSON malformado con 400. No conviertas silenciosamente un parámetro ausente en un filtro inesperado.

En un handler que recibe CreateBook, la decodificación debe acotar el cuerpo y rechazar una segunda entidad JSON. Con encoding/json e io importados:

go
r.Body = http.MaxBytesReader(w, r.Body, 64<<10)
decoder := json.NewDecoder(r.Body)
decoder.DisallowUnknownFields()
var input CreateBook
if err := decoder.Decode(&input); err != nil {
    http.Error(w, "JSON inválido o demasiado grande", 400)
    return
}
if err := decoder.Decode(&struct{}{}); err != io.EOF {
    http.Error(w, "Se esperaba un único objeto", 400)
    return
}
if strings.TrimSpace(input.Title) == "" || input.Pages <= 0 {
    http.Error(w, "Campos inválidos", 400)
    return
}

Importa strings y net/http. Esta política del laboratorio agrupa esos rechazos en 400; otra API puede documentar 413 para el tamaño excedido, pero debe hacerlo expresamente.

Pruébalo

  1. Envía GET /books/42?author=Ríos y registra ambos valores.

  2. Envía POST /books con {"title":"Rutas","pages":120}.

  3. Repite con pages:0 y con una coma sobrante en el JSON.

  4. Compara las respuestas.

Error frecuente: leer id desde la query aunque está en la ruta. Corrígelo usando mux.Vars para el segmento y URL.Query solo para filtros.

Comprueba tu resultado

El primer caso identifica 42 y Ríos; el JSON válido pasa; pages:0 y el cuerpo inválido reciben 400. La respuesta nunca debe afirmar que se creó un libro rechazado.

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 ↗

Tipo de lección: 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: "juan@example.com", 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.

Sigue con Go para APIs de Alto Rendimiento: Construcción de Microservicios REST con gorilla/mux

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.