StackPractices
intermediate Por Mathias Paulenko

Diseñar un API Gateway Escalable para Microservicios

Construí un gateway de API que enrute requests, maneje autenticación, rate limiting, caching y traducción de protocolos entre clientes y microservicios backend.

Overview

flowchart diagram: Client[Client

En una arquitectura de microservicios, los clientes pueden terminar hablando con decenas de servicios distintos, cada uno con su propio endpoint, protocolo y reglas de auth. Exponer todo eso directamente genera un desastre: cada cliente tiene que rastrear ubicaciones, manejar retries y administrar tokens separados. Cuando un servicio se mueve o aparece uno nuevo, cada cliente necesita una actualización.

Me topé con este dolor en una plataforma con 14 microservicios. Los clientes mobile tenían hardcodeadas 14 base URLs y re-implementaban validación JWT en tres lenguajes. Cuando agregamos un API gateway, el equipo mobile borró 2,000 líneas de boilerplate de la noche a la mañana. Una URL, un check de auth, un solo lugar para poner concerns cross-cutting.

Un API gateway se ubica al frente y se convierte en el único punto de entrada. Los clientes llaman a una URL y el gateway reenvía el request al backend correcto. También maneja concerns cross-cutting — autenticación, rate limiting, terminación SSL y transformación de request/response — para que los servicios internos no tengan que hacerlo. Para el panorama completo de topología de microservicios, mirá nuestra recipe de microservices patterns.

Cuándo Usarlo

Un API gateway empieza a valer la pena cuando varios servicios backend están expuestos a clientes, cuando auth y rate limiting necesitan centralizarse, o cuando distintos tipos de clientes como web, mobile e IoT necesitan formas de API diferentes. También ayuda durante una migración de monolito a microservicios porque permite mantener estable el contrato externo mientras cambia la topología interna. Finalmente, es el lugar correcto para poner una API GraphQL por encima de microservicios REST.

Cuándo NO Usarlo

Un gateway es exceso para uno o dos servicios; un reverse proxy o load balancer simple maneja eso. Saltearlo si el equipo no está listo para operarlo como infraestructura crítica con HA y monitoreo, si la lógica de negocio se filtra al gateway, o si el proyecto es pequeño y el overhead operacional no se justifica.

Solución

Kong Gateway (declarativo)

# kong.yml
_format_version: "3.0"
services:
  - name: user-service
    url: http://users.internal:8080
    routes:
      - name: user-routes
        paths:
          - /api/v1/users
    plugins:
      - name: rate-limiting
        config:
          minute: 100
          policy: redis
      - name: jwt
        config:
          claims_to_verify:
            - exp
      - name: proxy-cache
        config:
          response_code:
            - 200
          request_method:
            - GET
          cache_ttl: 300
          strategy: memory

  - name: order-service
    url: http://orders.internal:8080
    routes:
      - name: order-routes
        paths:
          - /api/v1/orders

Gateway custom en Node.js

const express = require('express');
const { createProxyMiddleware } = require('http-proxy-middleware');
const rateLimit = require('express-rate-limit');
const jwt = require('jsonwebtoken');

const app = express();

const limiter = rateLimit({
  windowMs: 60 * 1000,
  max: 100,
  standardHeaders: true,
});
app.use('/api/', limiter);

app.use('/api/', (req, res, next) => {
  const token = req.headers.authorization?.replace('Bearer ', '');
  if (!token) return res.status(401).json({ error: 'Missing token' });

  try {
    req.user = jwt.verify(token, process.env.JWT_SECRET);
    next();
  } catch {
    res.status(401).json({ error: 'Invalid token' });
  }
});

const services = {
  '/api/v1/users': 'http://users.internal:8080',
  '/api/v1/orders': 'http://orders.internal:8080',
};

Object.entries(services).forEach(([path, target]) => {
  app.use(path, createProxyMiddleware({
    target,
    changeOrigin: true,
    pathRewrite: { [`^${path}`]: '' },
    onProxyReq: (proxyReq, req) => {
      proxyReq.setHeader('X-User-Id', req.user.sub);
    },
  }));
});

app.get('/health', (req, res) => res.json({ status: 'ok' }));
app.listen(3000, () => console.log('Gateway running on port 3000'));

Traefik con labels de Docker

# docker-compose.yml
services:
  user-service:
    image: myregistry/user-service:latest
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.user-service.rule=PathPrefix(`/api/v1/users`)"
      - "traefik.http.routers.user-service.entrypoints=websecure"
      - "traefik.http.services.user-service.loadbalancer.server.port=8080"
      - "traefik.http.middlewares.user-ratelimit.ratelimit.average=100"
      - "traefik.http.routers.user-service.middlewares=user-ratelimit"

Apollo Router para GraphQL

# router.yaml
supergraph:
  listen: 0.0.0.0:4000
  path: /
  introspection: true

telemetry:
  exporters:
    tracing:
      otlp:
        endpoint: http://otel-collector:4317

Explicación

El enrutamiento de requests es el trabajo principal del gateway. Mapea paths entrantes a servicios backend: /api/v1/users va al servicio de usuarios, y ese servicio puede moverse o escalar sin que el cliente lo sepa.

Concerns como auth, rate limiting y caching pertenecen al borde. Resolverlos una vez allí evita duplicar la misma lógica en cada microservicio. Trabajé en equipos que saltearon esto y terminaron con validación JWT copy-pasteada across 12 servicios — una pesadilla de mantenimiento cuando el formato del token cambió.

Un gateway GraphQL puede disparar varios requests REST a microservicios y armar una sola respuesta tipada para el cliente. Esa es la traducción de protocolos en la práctica. El cliente pide { user { orders { status } } } y el gateway fetcha del servicio de usuarios, después del servicio de órdenes, y stitches la respuesta.

La terminación SSL significa que el gateway maneja TLS para que los servicios internos usen HTTP simple dentro de una red confiable. Esto saca el overhead de TLS de las llamadas internas y centraliza el manejo de certificados en el borde donde corresponde.

Lifecycle del request

  1. El cliente envía el request a la URL del gateway.
  2. El gateway valida el JWT o API key.
  3. El rate limiter verifica si el cliente está dentro de los límites.
  4. El router matchea el path y elige el backend.
  5. Cache check opcional — devuelve la respuesta cacheada si está disponible.
  6. El proxy reenvía el request al backend.
  7. La respuesta vuelve, transformación opcional, y se devuelve al cliente.

Los pasos 2–4 pasan en milisegundos. El gateway agrega latencia, pero ahorra round trips con caching, connection pooling y short-circuiting de requests inválidos antes de que lleguen al backend.

Variantes

TipoGestiónIdeal paraCompromiso
Self-hosted (Kong, Traefik)Control totalOn-prem, complianceOverhead operacional
Managed (AWS, Azure, GCP)ServerlessCloud-native, escalarVendor lock-in, costo
Custom builtFlexibilidad máximaRequisitos únicosCosto de desarrollo
Service mesh (Istio ingress)Kubernetes-nativeClusters K8sComplejidad

Buenas Prácticas

  • Implementá circuit breakers en el gateway para dejar de enviar tráfico a backends con fallos.
  • Usá versionado en el path, como /api/v1/users, en vez de headers. Mantiene el routing explícito y las claves de caché simples.
  • Centralizá observability: inyectá trace IDs en el borde y propagalos downstream.
  • Descargá autenticación validando JWTs o API keys en el gateway y reenviá headers de contexto de usuario a los backends.
  • Cacheá endpoints read-heavy en el borde, como catálogos de productos y datos de configuración.

Errores Comunes

  • Tratar al gateway como un servicio más y meter lógica de negocio. Dejá routing, auth y rate limiting en el borde; las reglas de negocio van en los servicios de dominio.
  • Publicar sin timeouts ni reglas de retry. Definí timeouts por ruta y reintentá solo operaciones idempotentes.
  • Correr una sola instancia del gateway. Usá al menos dos instancias detrás de un load balancer con health checks.
  • Ignorar necesidades específicas de clientes. Las apps mobile suelen necesitar payloads más pequeños que las web, así que un gateway backend-for-frontend (BFF) puede valer la pena.

See Also

Preguntas frecuentes

¿Uso un API gateway o un service mesh?

Usá un gateway para tráfico north-south — clientes externos hacia el cluster. Usá un service mesh para tráfico east-west — servicios hablando entre sí dentro del cluster. Se complementan.

¿Cómo manejo GraphQL en un gateway?

Elegí un gateway GraphQL como Apollo Router o Hasura. Cada microservicio expone un subgraph y el gateway los une en un supergraph.

¿Agrega latencia un gateway?

Sí, pero generalmente solo 1–5 ms para un gateway bien afinado. Los beneficios — caching, connection pooling y auth centralizado — suelen reducir la latencia total.

¿Cómo aseguro llamadas servicio a servicio detrás de un gateway?

El gateway valida tokens externos. Para llamadas internas, usá mTLS o tokens internos firmados. Nunca confiés en headers de auth orientados al usuario para llamadas internas.

¿Un gateway puede reemplazar un load balancer?

No exactamente. Un gateway maneja routing, auth y traducción de protocolos. Un load balancer distribuye tráfico across instancias del mismo servicio. En la práctica, los gateways viven detrás de un load balancer para alta disponibilidad — el LB enruta a instancias saludables del gateway, y cada gateway enruta al backend correcto.

¿Por qué mi gateway agrega latencia incluso en cache hits?

El gateway igual tiene que parsear el request, validar auth, buscar la cache key y serializar la respuesta. Eso suele ser 1–3 ms en cache hits. Si ves más, revisá tu cache backend — un cache in-memory es más rápido que Redis para claves calientes, pero Redis gana cuando tenés dos o más instancias del gateway.