Desarrollo backend

API Gateway desde cero en Go: enrutamiento, autenticación y rate limiting

Ruslan Ismailov Publicado 14 min de lectura
A

Introducción: ¿por qué construir tu propio API Gateway?

Un API Gateway es el punto de entrada único para todas las solicitudes de los clientes en una arquitectura de microservicios. Se encarga del enrutamiento, la autenticación, el balanceo de carga y la limitación de peticiones, liberando a los servicios de negocio de la lógica transversal.

Las soluciones listas para usar —Kong, Traefik, AWS API Gateway— cubren la mayoría de los casos. Sin embargo, hay escenarios en los que una implementación propia en Go está justificada: requisitos estrictos de rendimiento, protocolos de autenticación no estándar, control total sobre la lógica de manejo de errores o simplemente restricciones de licencias. Go es ideal para esta tarea: baja sobrecarga, concurrencia nativa y una rica biblioteca estándar.

Arquitectura del API Gateway: componentes principales

Un buen API Gateway se compone de las siguientes capas:

  • Router — asocia la solicitud entrante con el microservicio destino según la ruta, el método y las cabeceras.
  • Middleware chain — secuencia de manejadores: autenticación, rate limiting, logging, trazado.
  • Reverse proxy — redirige la solicitud al servicio upstream y devuelve la respuesta al cliente.
  • Circuit breaker / Retry — protege el sistema de fallos en cascada.
  • Observability — recopilación de métricas, logs y trazas.

Implementación del enrutamiento de solicitudes a microservicios

Para el enrutamiento usaremos el popular router gorilla/mux o el paquete integrado net/http. A continuación, una implementación minimalista de reverse proxy con enrutamiento dinámico.

package main\n\nimport (\n    \"log\"\n    \"net/http\"\n    \"net/http/httputil\"\n    \"net/url\"\n)\n\ntype Route struct {\n    Prefix  string\n    Target  string\n}\n\nvar routes = []Route{\n    {Prefix: \"/users\", Target: \"http://user-service:8081\"},\n    {Prefix: \"/orders\", Target: \"http://order-service:8082\"},\n    {Prefix: \"/products\", Target: \"http://product-service:8083\"},\n}\n\nfunc proxyHandler(target string) http.Handler {\n    url, _ := url.Parse(target)\n    proxy := httputil.NewSingleHostReverseProxy(url)\n    proxy.ModifyResponse = func(resp *http.Response) error {\n        resp.Header.Set(\"X-Gateway\", \"go-gateway/1.0\")\n        return nil\n    }\n    return proxy\n}\n\nfunc main() {\n    mux := http.NewServeMux()\n    for _, r := range routes {\n        handler := proxyHandler(r.Target)\n        mux.Handle(r.Prefix+\"/\", http.StripPrefix(r.Prefix, handler))\n    }\n    log.Println(\"Gateway listening on :8080\")\n    log.Fatal(http.ListenAndServe(\":8080\", mux))\n}

El router recorre las rutas registradas, encuentra la coincidencia por prefijo y redirige la solicitud a través de httputil.ReverseProxy. Para un enrutamiento más avanzado (parámetros de ruta, regex) utiliza chi o gorilla/mux.

Autenticación y autorización: JWT y middleware

La autenticación a nivel del Gateway evita que cada microservicio deba verificar tokens por separado. Implementamos un middleware para validar JWT usando la librería golang-jwt/jwt.

package middleware\n\nimport (\n    \"context\"\n    \"fmt\"\n    \"net/http\"\n    \"strings\"\n\n    \"github.com/golang-jwt/jwt/v5\"\n)\n\ntype contextKey string\nconst UserIDKey contextKey = \"userID\"\n\nvar jwtSecret = []byte(\"super-secret-key\")\n\nfunc JWTAuth(next http.Handler) http.Handler {\n    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n        authHeader := r.Header.Get(\"Authorization\")\n        if !strings.HasPrefix(authHeader, \"Bearer \") {\n            http.Error(w, \"missing or invalid token\", http.StatusUnauthorized)\n            return\n        }\n        tokenStr := strings.TrimPrefix(authHeader, \"Bearer \")\n        token, err := jwt.Parse(tokenStr, func(t *jwt.Token) (interface{}, error) {\n            if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok {\n                return nil, fmt.Errorf(\"unexpected signing method: %v\", t.Header[\"alg\"])\n            }\n            return jwtSecret, nil\n        })\n        if err != nil || !token.Valid {\n            http.Error(w, \"unauthorized\", http.StatusUnauthorized)\n            return\n        }\n        claims, _ := token.Claims.(jwt.MapClaims)\n        userID := claims[\"sub\"].(string)\n        ctx := context.WithValue(r.Context(), UserIDKey, userID)\n        // Pasamos userID en la cabecera para los servicios downstream\n        r.Header.Set(\"X-User-ID\", userID)\n        next.ServeHTTP(w, r.WithContext(ctx))\n    })\n}

El middleware extrae el userID de los claims del JWT, lo coloca en el contexto de la solicitud y añade la cabecera X-User-ID, que los servicios downstream pueden leer directamente sin necesidad de volver a parsear el token.

Rate limiting: Token Bucket y Sliding Window con Redis

El rate limiting protege los microservicios de la sobrecarga. Analizamos dos algoritmos populares y su implementación con Redis.

Token Bucket

Los tokens se acumulan a una velocidad constante y se consumen con cada solicitud. Lo implementamos mediante operaciones atómicas en Redis:

package ratelimit\n\nimport (\n    \"context\"\n    \"fmt\"\n    \"time\"\n\n    \"github.com/redis/go-redis/v9\"\n)\n\ntype TokenBucketLimiter struct {\n    client   *redis.Client\n    capacity int64\n    rate     int64 // tokens por segundo\n}\n\nfunc (l *TokenBucketLimiter) Allow(ctx context.Context, key string) (bool, error) {\n    now := time.Now().Unix()\n    bucketKey := fmt.Sprintf(\"tb:%s\", key)\n\n    pipe := l.client.TxPipeline()\n    getTokens := pipe.Get(ctx, bucketKey)\n    _, err := pipe.Exec(ctx)\n    _ = err\n\n    tokens, _ := getTokens.Int64()\n    if tokens <= 0 {\n        tokens = l.capacity\n    }\n    _ = now\n\n    if tokens > 0 {\n        l.client.Decr(ctx, bucketKey)\n        l.client.Expire(ctx, bucketKey, time.Second*60)\n        return true, nil\n    }\n    return false, nil\n}\n

Sliding Window con Redis

Un algoritmo más preciso: contamos el número de solicitudes en una ventana temporal deslizante usando un Sorted Set en Redis.

package ratelimit\n\nimport (\n    \"context\"\n    \"fmt\"\n    \"time\"\n\n    \"github.com/redis/go-redis/v9\"\n)\n\ntype SlidingWindowLimiter struct {\n    client  *redis.Client\n    limit   int64\n    window  time.Duration\n}\n\nfunc (l *SlidingWindowLimiter) Allow(ctx context.Context, key string) (bool, error) {\n    now := time.Now()\n    windowStart := now.Add(-l.window).UnixMilli()\n    swKey := fmt.Sprintf(\"sw:%s\", key)\n\n    pipe := l.client.TxPipeline()\n    pipe.ZRemRangeByScore(ctx, swKey, \"0\", fmt.Sprintf(\"%d\", windowStart))\n    count := pipe.ZCard(ctx, swKey)\n    pipe.ZAdd(ctx, swKey, redis.Z{\n        Score:  float64(now.UnixMilli()),\n        Member: now.UnixNano(),\n    })\n    pipe.Expire(ctx, swKey, l.window)\n    _, err := pipe.Exec(ctx)\n    if err != nil {\n        return false, err\n    }\n    return count.Val() < l.limit, nil\n}\n

El middleware de rate limiting se inserta en la cadena antes de la autenticación (protección contra DDoS en endpoints abiertos) o después de ella (límites personalizados por userID):

func RateLimitMiddleware(limiter *SlidingWindowLimiter) func(http.Handler) http.Handler {\n    return func(next http.Handler) http.Handler {\n        return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n            key := r.RemoteAddr // o userID del contexto\n            allowed, err := limiter.Allow(r.Context(), key)\n            if err != nil || !allowed {\n                http.Error(w, \"rate limit exceeded\", http.StatusTooManyRequests)\n                return\n            }\n            next.ServeHTTP(w, r)\n        })\n    }\n}

Manejo de errores: Circuit Breaker y Retry

Cuando un servicio upstream degrada, el Gateway debe responder de forma correcta. Usamos la librería sony/gobreaker para el circuit breaker:

package circuit\n\nimport (\n    \"net/http\"\n    \"time\"\n\n    \"github.com/sony/gobreaker\"\n)\n\nfunc NewBreakerProxy(target string, next http.Handler) http.Handler {\n    cb := gobreaker.NewCircuitBreaker(gobreaker.Settings{\n        Name:        target,\n        MaxRequests: 5,\n        Interval:    10 * time.Second,\n        Timeout:     30 * time.Second,\n        ReadyToTrip: func(counts gobreaker.Counts) bool {\n            return counts.ConsecutiveFailures > 3\n        },\n    })\n\n    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n        _, err := cb.Execute(func() (interface{}, error) {\n            rr := &responseRecorder{ResponseWriter: w}\n            next.ServeHTTP(rr, r)\n            if rr.statusCode >= 500 {\n                return nil, fmt.Errorf(\"upstream error: %d\", rr.statusCode)\n            }\n            return nil, nil\n        })\n        if err != nil {\n            http.Error(w, \"service unavailable\", http.StatusServiceUnavailable)\n        }\n    })\n}

Para la lógica de retry, utiliza backoff exponencial: reintenta la solicitud como máximo 3 veces con retrasos de 100ms, 200ms y 400ms antes de devolver un error al cliente.

Logging y trazado de solicitudes

Cada solicitud que pasa por el Gateway debe recibir un trace ID único, que se propaga en la cabecera X-Request-ID a todas las llamadas downstream. Usamos go.opentelemetry.io/otel para el trazado distribuido:

func TracingMiddleware(tracer trace.Tracer) func(http.Handler) http.Handler {\n    return func(next http.Handler) http.Handler {\n        return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n            ctx, span := tracer.Start(r.Context(), r.URL.Path)\n            defer span.End()\n\n            requestID := r.Header.Get(\"X-Request-ID\")\n            if requestID == \"\" {\n                requestID = uuid.New().String()\n            }\n            span.SetAttributes(attribute.String(\"request.id\", requestID))\n            r.Header.Set(\"X-Request-ID\", requestID)\n\n            next.ServeHTTP(w, r.WithContext(ctx))\n        })\n    }\n}

El logging estructurado con slog (Go 1.21+) facilita el parseo de logs en ELK o Loki:

slog.Info(\"request\",\n    \"method\", r.Method,\n    \"path\", r.URL.Path,\n    \"duration_ms\", time.Since(start).Milliseconds(),\n    \"status\", statusCode,\n    \"request_id\", requestID,\n)

Despliegue del API Gateway en Kubernetes

El API Gateway se despliega como un Deployment con HPA para el escalado automático. Ejemplo de manifiesto:

apiVersion: apps/v1\nkind: Deployment\nmetadata:\n  name: api-gateway\nspec:\n  replicas: 3\n  selector:\n    matchLabels:\n      app: api-gateway\n  template:\n    metadata:\n      labels:\n        app: api-gateway\n    spec:\n      containers:\n      - name: gateway\n        image: myregistry/api-gateway:v1.0.0\n        ports:\n        - containerPort: 8080\n        env:\n        - name: REDIS_URL\n          valueFrom:\n            secretKeyRef:\n              name: redis-secret\n              key: url\n        - name: JWT_SECRET\n          valueFrom:\n            secretKeyRef:\n              name: jwt-secret\n              key: value\n        resources:\n          requests:\n            cpu: \"100m\"\n            memory: \"128Mi\"\n          limits:\n            cpu: \"500m\"\n            memory: \"512Mi\"\n        readinessProbe:\n          httpGet:\n            path: /healthz\n            port: 8080\n          initialDelaySeconds: 5\n          periodSeconds: 10\n---\napiVersion: v1\nkind: Service\nmetadata:\n  name: api-gateway-svc\nspec:\n  type: LoadBalancer\n  selector:\n    app: api-gateway\n  ports:\n  - port: 80\n    targetPort: 8080

En Kubernetes es fundamental configurar readiness y liveness probes para que el tráfico no se dirija a pods durante el arranque en frío o cuando el servicio degrada. Para almacenar la configuración de rutas, utiliza ConfigMap con recarga en caliente mediante fsnotify.

Comparativa con soluciones existentes: ¿cuándo vale la pena un Gateway propio?

CriterioGateway propio en GoKong / Traefik
Tiempo hasta producción2–4 semanas1–3 días
Flexibilidad de lógicaMáximaLimitada por plugins
RendimientoOptimizado para el caso de usoAlto, pero con sobrecarga
Carga operativaAlta (mantenimiento del código)Baja
CosteTiempo de los desarrolladoresGratuito / Licencias Enterprise

Un API Gateway propio en Go está justificado cuando:

  • Se requieren protocolos de autenticación no estándar o lógica de enrutamiento de negocio que no puede expresarse mediante configuración.
  • Existen requisitos extremos de latencia (overhead sub-millisecond).
  • Se necesita control total sobre las dependencias y ausencia de vendor lock-in.
  • El equipo domina Go y está preparado para mantener el código.

En los demás casos, Traefik junto con Kubernetes Ingress o Kong con plugins cubrirá el 95% de las necesidades de forma más rápida y fiable.

Conclusión

Hemos construido un API Gateway completo en Go: desde el enrutamiento dinámico de solicitudes a microservicios hasta la autenticación JWT, rate limiting con Redis (Token Bucket y Sliding Window), circuit breaker, trazado distribuido y despliegue en Kubernetes. Go es ideal para esta tarea: mínima sobrecarga, potente biblioteca estándar y un modelo de concurrencia sencillo.

Principios clave al desarrollar tu propio Gateway: mantén la cadena de middleware lineal y testeable, externaliza la configuración de rutas a una fuente con recarga en caliente (Redis, ConfigMap) y nunca descuides la observabilidad desde el primer día. Un Gateway bien diseñado se convierte en la base sólida de toda la plataforma de microservicios.

Tecnologías

Etiquetas

Ruslan Ismailov

Desarrollador Senior Web / Backend. Desarrollador senior web/backend con 9 años de experiencia. Stack: PHP, Laravel, PostgreSQL, Redis, Docker, Kubernetes, REST, microservicios, CI/CD. Más sobre mí →