REST API en Go con Gin y Middleware
Construye APIs REST listas para producción en Go usando el framework Gin con middleware custom para logging, autenticación, validación y manejo de errores.
Visión General
La mayoría de las APIs en Go empiezan como un par de http.HandleFunc y después
se convierten en un desastre silencioso: el mismo chequeo de auth copiado en
cada handler, respuestas de error que cambian en cada ruta y lógica de
middleware esparcida en cualquier lado. Gin te da una salida limpia: un router
rápido más una cadena de middleware que podés testear por separado.
En esta receta armo la configuración a la que recurro cuando necesito una API en Go chica que se comporte como un servicio real: logging de requests, auth por token, validación de JSON, errores estructurados y graceful shutdown. El código está separado en archivos pequeños para que puedas meter las piezas en tu propio proyecto, y hay un companion ejecutable en el repo stack-practices-resources si querés evitar el copiar y pegar.
Cuándo Usar
- Usá Gin cuando querés un router rápido y middleware reusable, no un framework que lo haga todo. Yo lo usé para servicios internos y APIs públicas donde necesitaba baja latencia sin tanto ritual.
- Usalo cuando las preocupaciones transversales como logging, auth y métricas deben correr antes de cada ruta en lugar de copiarse en cada handler.
- Es un buen fit cuando la API sirve a SPAs, clientes móviles u otros backends. Consultá Llamar REST API para patrones de cliente.
- Evitalo si estás armando un solo
http.HandleFuncdetrás de un load balancer y no necesitás middleware. Para esos casos,net/httpcon un router mínimo alcanza y te deja la lista de dependencias más corta.
Solución
Setup básico del servidor
// main.go
package main
import (
"log"
"net/http"
"github.com/gin-gonic/gin"
"example.com/go-rest-api-gin/handlers"
"example.com/go-rest-api-gin/middleware"
"example.com/go-rest-api-gin/server"
)
func main() {
gin.SetMode(gin.ReleaseMode)
r := gin.New()
r.Use(middleware.Logger(), gin.Recovery(), middleware.ErrorHandler())
api := r.Group("/api/v1")
api.Use(middleware.AuthRequired())
{
api.GET("/users", handlers.ListUsers)
api.GET("/users/:id", handlers.GetUser)
api.POST("/users", handlers.CreateUser)
api.GET("/health", handlers.Health)
}
if err := server.RunWithGracefulShutdown(r, ":8080"); err != nil {
log.Fatalf("server shutdown: %s", err)
}
}
Middleware custom
// middleware/logger.go
package middleware
import (
"log"
"time"
"github.com/gin-gonic/gin"
)
func Logger() gin.HandlerFunc {
return func(c *gin.Context) {
start := time.Now()
path := c.Request.URL.Path
c.Next()
latency := time.Since(start)
status := c.Writer.Status()
log.Printf("[%s] %s %d %v", c.Request.Method, path, status, latency)
}
}
// middleware/auth.go
package middleware
import (
"net/http"
"github.com/gin-gonic/gin"
)
func AuthRequired() gin.HandlerFunc {
return func(c *gin.Context) {
token := c.GetHeader("Authorization")
if token == "" {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "missing token"})
return
}
c.Set("user", token)
c.Next()
}
}
Validación de requests
// handlers/user.go
package handlers
import (
"net/http"
"github.com/gin-gonic/gin"
"example.com/go-rest-api-gin/middleware"
)
type CreateUserRequest struct {
Name string `json:"name" binding:"required,min=2,max=50"`
Email string `json:"email" binding:"required,email"`
Age int `json:"age" binding:"gte=0,lte=150"`
}
func ListUsers(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"users": []string{"alice", "bob"}})
}
func GetUser(c *gin.Context) {
id := c.Param("id")
c.JSON(http.StatusOK, gin.H{"id": id})
}
func CreateUser(c *gin.Context) {
var req CreateUserRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.Error(&middleware.APIError{Code: "validation_failed", Message: err.Error(), Status: http.StatusBadRequest})
return
}
// Reemplazá con tu lógica real de persistencia.
user := gin.H{"id": 1, "name": req.Name, "email": req.Email, "age": req.Age}
c.JSON(http.StatusCreated, user)
}
func Health(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"status": "healthy"})
}
Manejo estructurado de errores
// middleware/error.go
package middleware
import (
"net/http"
"github.com/gin-gonic/gin"
)
type APIError struct {
Code string `json:"code"`
Message string `json:"message"`
Status int `json:"-"`
}
func (e *APIError) Error() string { return e.Message }
func ErrorHandler() gin.HandlerFunc {
return func(c *gin.Context) {
c.Next()
if len(c.Errors) == 0 {
return
}
err := c.Errors.Last().Err
if apiErr, ok := err.(*APIError); ok {
c.JSON(apiErr.Status, apiErr)
return
}
c.JSON(http.StatusInternalServerError, gin.H{"error": "internal error"})
}
}
Adjuntá errores al contexto con c.Error(err) en handlers o middleware. El error
handler corre después de c.Next() y escribe una respuesta consistente.
Graceful shutdown
// server/server.go
package server
import (
"context"
"log"
"net/http"
"os"
"os/signal"
"syscall"
"time"
"github.com/gin-gonic/gin"
)
func RunWithGracefulShutdown(router *gin.Engine, addr string) error {
srv := &http.Server{
Addr: addr,
Handler: router,
}
go func() {
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Fatalf("listen: %s", err)
}
}()
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
return srv.Shutdown(ctx)
}
Explicación
Gin mantiene el procesamiento de requests rápido porque construye un árbol
radix para las rutas y reusa buffers para JSON. La verdadera ventaja, sin
embargo, es la cadena de middleware: cada request pasa por los handlers
registrados en orden, y cada uno decide si sigue con c.Next() o se frena con
c.Abort().
Este es el ciclo de vida que tengo en mente cuando armo un servicio con Gin:
El paquete binding valida y llena una struct desde JSON. Te ahorra mucho
boilerplate de json.Unmarshal, pero solo chequea que la forma del input sea la
correcta. No te va a decir si un email ya está en uso o si un usuario supera una
cuota de negocio. Siempre agregá validación propia después del binding.
El error handler se sienta al final de la cadena. Cuando un handler o middleware
adjunta un error con c.Error(err), Gin lo acumula en c.Errors. Después de
que el handler retorna, el middleware de errores revisa esa lista y escribe un
único JSON consistente. Esto evita llamar c.JSON dos veces y evita que los
clientes vean strings de error crudos de Go.
El graceful shutdown envuelve el http.Server de la librería estándar. Iniciá
el server en una goroutine, esperá SIGINT o SIGTERM y después das a los
requests en vuelo un presupuesto de tiempo para terminar. Yo suelo poner ese
presupuesto en cinco segundos; más que eso y el orquestador termina matando el
contenedor igual.
Variantes
Route groups con rate limiting
Usá esta variante cuando querés proteger un subconjunto de rutas o toda la API
contra un pico de tráfico. Para una sola instancia, mantené un limiter por IP de
cliente; para un despliegue distribuido, mové el estado a Redis y agregá un
header Retry-After en las respuestas 429.
import "golang.org/x/time/rate"
func RateLimiter(rps float64, burst int) gin.HandlerFunc {
limiter := rate.NewLimiter(rate.Limit(rps), burst)
return func(c *gin.Context) {
if !limiter.Allow() {
c.AbortWithStatusJSON(http.StatusTooManyRequests, gin.H{"error": "rate limit exceeded"})
return
}
c.Next()
}
}
api := r.Group("/api/v1")
api.Use(RateLimiter(10, 20))
Para una implementación distribuida consultá Rate Limiting with Redis.
CORS
El snippet de abajo permite un origen conocido, los métodos HTTP comunes y
credentials. Yo suelo agregar OPTIONS y PATCH a AllowMethods para no tener
que volver más tarde cuando un frontend empiece a usarlos.
import "github.com/gin-contrib/cors"
r.Use(cors.New(cors.Config{
AllowOrigins: []string{"https://yourdomain.com"},
AllowMethods: []string{"GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"},
AllowHeaders: []string{"Origin", "Content-Type", "Authorization"},
AllowCredentials: true,
MaxAge: 12 * time.Hour,
}))
Cuándo elegir gin.New() sobre gin.Default()
| Setup | Middleware incluido | Mejor para |
|---|---|---|
gin.Default() | Logger + Recovery | Prototipos, servicios pequeños |
gin.New() | Ninguno | Servicios en producción con logging, manejo de errores y auth custom |
gin.Default() agrega el logger y recovery por defecto. Yo suelo recurrir a
gin.New() cuando quiero controlar el orden y elegir mi propio logger. Si
querés un prototipo rápido, gin.Default() está bien; acordate de que su logger
incorporado puede entrar en conflicto con el que agregués después.
Mejores Prácticas
- En producción siempre llamo
gin.SetMode(gin.ReleaseMode)para que Gin deje de imprimir debug output y logs con colores a stdout. - Uso
gin.New()cuando quiero elegir el orden exacto de los middleware.gin.Default()está bien para prototipos, pero ya incluye un logger y recovery que pueden chocar con los custom. - Para logging cambio el paquete
logde la librería estándar porzapozerolog. Los logs estructurados son mucho más fáciles de mandar a ELK, Datadog o CloudWatch. - Mantené cada middleware enfocado. Yo escribo uno para logging, otro para auth, otro para errores. Mezclar responsabilidades hace los tests complicados.
- Devolvé errores tipados desde los servicios y dejá que el middleware de errores decida el formato JSON final.
- Cuando hagas load tests, mirá los percentiles p95 y p99, no el promedio. El promedio oculta los requests lentos que los usuarios sí notan.
Errores Comunes
- Usar
gin.Default()y después agregar middleware de logger o recovery custom. Los built-in siguen corriendo, así que podés loggear el mismo request dos veces o recuperar un panic que querías manejar vos. - Olvidar
c.Next()en el middleware, de modo que el handler nunca corre. - Llamar
c.Abort()sin escribir una respuesta, dejando al cliente colgado hasta que le de timeout. - Mantener una conexión a base de datos dentro del context sin un pool de conexiones. El pool espera que le devuelvan la conexión mientras el request termina.
- Confiar solo en la validación del binding para reglas de negocio. Solo chequea la forma; no va a detectar duplicados, límites de cuota o chequeos de autorización.
- Devolver errores crudos de Go a los clientes en lugar de un objeto de error
estructurado. El cliente no puede parsear
pq: duplicate keyosql: no rows; sí puede parsear{"code":"..."}.
Ver También
La documentación de Gin es la referencia oficial.
El paquete net/http de Go cubre los primitivos del server.
golang.org/x/time/rate explica el rate limiting con token bucket.
Para agregados de Gin, consultá el middleware CORS y el
generador OpenAPI Swaggo. Si querés exponer gRPC sobre HTTP, mirá
gRPC-Gateway.
Preguntas frecuentes
¿Qué agrega Gin sobre el paquete estándar net/http?
Gin agrega routing, middleware, binding de requests y recuperación de panics con
mínimo overhead. Para APIs muy pequeñas, net/http con un router como chi
también es suficiente.
¿Puedo usar Gin con gRPC?
Sí. Podés correr servidores gRPC y HTTP lado a lado, o usar grpc-gateway para
exponer endpoints HTTP generados desde definiciones protobuf.
¿Cómo estructuro una aplicación Gin grande?
Agrupá rutas por dominio con gin.RouterGroup. Mantené archivos de rutas por
dominio (routes/users.go, routes/orders.go) y registrá todo en main.go.
Pasá las dependencias a los handlers a través de una struct en lugar de variables
globales, y usá repositorios basados en interfaces para que los handlers sean
fáciles de testear.
¿Cuándo uso ShouldBindJSON en lugar de BindJSON?
Usá ShouldBindJSON cuando querés manejar el error vos mismo. Devuelve un error
en lugar de escribir directamente una respuesta 400, lo que te da control sobre
el formato JSON.
¿Cuál es la forma más limpia de manejar errores en Gin?
Definí un tipo de error pequeño con code y message, devolvelo desde los
servicios, adjuntalo con c.Error() y dejá que un middleware de errores escriba
el JSON final. Logueá el error original con un request ID para tracing.
¿Cómo testeo handlers de Gin sin levantar un server?
Usá httptest.NewRecorder() y router.ServeHTTP para llamar handlers sin
levantar un server. Creá un router con dependencias mockeadas y asertá sobre el
status code, body y headers.
¿Cómo implemento rate limiting para un sistema distribuido?
Usá un token bucket como golang.org/x/time/rate como middleware. Para una sola
instancia, un limiter por IP de cliente funciona. Para sistemas distribuidos,
usá un limiter respaldado en Redis e incluí un header Retry-After en las
respuestas 429.
¿Por qué usar gin.New() en lugar de gin.Default()?
Usá gin.New() cuando querés controlar exactamente qué middleware corre y en qué
orden. gin.Default() está bien para prototipos rápidos porque ya incluye
middleware de logger y recovery.
¿Cómo uso Gin con OpenAPI/Swagger?
Usá swaggo/swag para generar docs OpenAPI desde anotaciones. Agregá comentarios
@Summary, @Param y @Router arriba de cada handler, ejecutá swag init y
serví la UI con ginSwagger. Mantené las anotaciones sincronizadas con las
signatures de los handlers.
¿Cómo aseguro rutas con JWT?
Extraé el token del header Authorization: Bearer <token>, validalo con una
librería JWT y guardá el user ID en el contexto con c.Set(). Devolvé 401
cuando el token falta, es inválido o expiró.
Recursos Relacionados
Implementa Server-Sent Events en Go para Actualizaciones
Construí un endpoint de Server-Sent Events en Go listo para producción con gestión de conexiones, heartbeats y manejo graceful de desconexiones de clientes.
RecipeImplementar Rate Limiting de APIs con Redis
Protege APIs de abuso usando algoritmos de token bucket y ventana deslizante con Redis, incluyendo manejo de bursts, coordinacion distribuida y headers custom para feedback al cliente
RecipePaginación por Cursor en PostgreSQL (Keyset vs OFFSET)
Implementa paginación eficiente por cursor para datasets grandes en PostgreSQL, evitando la degradación de rendimiento de OFFSET con paginación keyset indexada y ordenamiento estable
PatternChain of Responsibility para Middleware de Procesamiento
Pasa peticiones a lo largo de una cadena de handlers donde cada handler decide si procesa la peticion o la pasa al siguiente handler en la pipeline
RecipePatrones de Composicion de Middleware en Express.js
Construye aplicaciones Express mantenibles usando patrones de composicion de middleware para autenticacion, validacion, manejo de errores, propagacion de contexto y wrappers de rutas async
RecipeValidacion de Datos Basada en Schemas con Zod en TypeScript
Valida y sanitiza datos entrantes usando schemas Zod con inferencia de TypeScript, refinements custom y formateo de errores para validacion robusta de APIs y formularios