# Día 26: API Gateway, API-First Design y Contratos OpenAPI 3.1

> Spring Cloud Gateway, Enrutamiento Declarativo, Generación de Código con OpenAPI Generator

- Plan: [JavaMastery](https://aprende-java.webs-economicas.es/), día 26 de 30
- Fase 7: API Gateway, API-First, Arquitectura Hexagonal Enterprise y Sagas
- Dedicación: 4h
- Profundidad: visión de conjunto
- Revisado: 2026-10-29
- Versión interactiva: https://aprende-java.webs-economicas.es/dias/api-gateway-openapi/

## Al terminar este día sabrás

- Situar un API Gateway en la arquitectura y decidir qué responsabilidades le corresponden.
- Trabajar con un contrato OpenAPI como fuente de verdad y generar código a partir de él.
- Configurar rutas, predicados y filtros en Spring Cloud Gateway.
- Evolucionar una API sin romper a sus clientes.


## Conceptos

### 1. API Gateway: Punto de Entrada Único para Microservicios

Un API Gateway es un servicio que actúa como puerta de entrada centralizada para todas las peticiones de clientes externos. Recibe las llamadas HTTP, las enruta al microservicio correcto según la URL o cabeceras, y aplica preocupaciones transversales como autenticación, rate limiting, logging, transformación de headers y balanceo de carga.

**Cómo funciona por dentro.**

Un API Gateway es el único punto de entrada de los clientes externos a un conjunto de servicios. Recibe todas las peticiones y las encamina al servicio adecuado según la ruta, el método o las cabeceras. Así los clientes no conocen la topología interna, que puede cambiar sin afectarles.

Además del enrutamiento, concentra preocupaciones transversales que no conviene repetir en cada servicio: terminación TLS, validación de tokens, limitación de peticiones (rate limiting), CORS, cabeceras de seguridad, registro de accesos, propagación del id de correlación y, a veces, agregación de respuestas para un cliente concreto (patrón Backend for Frontend).

Como todo el tráfico pasa por él, debe desplegarse con alta disponibilidad (varias instancias tras un balanceador) y mantenerse ligero. Si se convierte en un lugar donde cada equipo añade lógica, pasa a ser un cuello de botella organizativo y técnico.

*Rutas declarativas en Spring Cloud Gateway*

```yaml
spring:
  cloud:
    gateway:
      default-filters:
        - AddResponseHeader=X-Content-Type-Options, nosniff
      routes:
        - id: tareas
          uri: http://tareas-service:8080
          predicates:
            - Path=/api/tareas/**
          filters:
            - name: CircuitBreaker
              args:
                name: tareas
                fallbackUri: forward:/fallback/tareas

        - id: usuarios-v2
          uri: http://usuarios-service-v2:8080
          predicates:
            - Path=/api/v2/usuarios/**
          filters:
            - StripPrefix=2                    # /api/v2/usuarios/7 -> /usuarios/7
            - name: RequestRateLimiter
              args:
                redis-rate-limiter.replenishRate: 20
                redis-rate-limiter.burstCapacity: 40
```

- **En una línea:** `spring.cloud.gateway.routes[0].uri=http://servicio-pedidos:8080
spring.cloud.gateway.routes[0].predicates[0]=Path=/api/v1/pedidos/**`
- **Por qué importa:** Evita que los clientes (frontends, apps móviles) deban conocer las IPs y puertos internos de cada microservicio. Centraliza seguridad y monitorización.
- **Error típico:** Poner lógica de negocio en el API Gateway (cálculos, validaciones de dominio) en vez de mantenerlo como un enrutador ligero sin estado.
- **Java 17 frente a 21:** Patrón independiente de versión.
- **Se conecta con:** Spring Cloud Gateway, Kong, Envoy, Ingress de Kubernetes

### 2. API-First / Contract-First: Diseñar la API Antes de Escribir Código

En el enfoque API-First, el equipo define primero el contrato de la API en un fichero YAML/JSON de OpenAPI 3.1 (paths, schemas, respuestas, errores) y lo revisa y aprueba con el equipo de frontend y stakeholders ANTES de escribir una sola línea de código Java. Después se genera código automáticamente (interfaces, DTOs) con herramientas como `openapi-generator`.

**Cómo funciona por dentro.**

En API-First (o Contract-First) el contrato de la API se escribe primero, normalmente en OpenAPI, se revisa con los consumidores y se versiona en el repositorio. A partir de él se genera código: interfaces y DTOs en el servidor, clientes en los consumidores, servidores simulados (mocks) para que el frontend trabaje antes de que exista el backend, y documentación.

El contrato es la fuente de verdad. El servidor implementa interfaces generadas, así que si el código se desvía del contrato, no compila. Los equipos pueden trabajar en paralelo y los cambios incompatibles se detectan en la revisión del fichero OpenAPI, antes de escribir una línea.

Con `openapi-generator-maven-plugin` y la opción `interfaceOnly`, se generan interfaces con las anotaciones de Spring y records o clases para los modelos; tu controlador solo implementa la interfaz.

*Contrato OpenAPI y generación en Maven*

```yaml
# src/main/resources/api/tareas.yaml
openapi: 3.1.0
info: { title: Tareas, version: 1.0.0 }
paths:
  /tareas/{id}:
    get:
      operationId: obtenerTarea
      parameters:
        - { name: id, in: path, required: true, schema: { type: integer, format: int64 } }
      responses:
        "200":
          description: La tarea
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Tarea" }
        "404": { description: No existe }
components:
  schemas:
    Tarea:
      type: object
      required: [id, titulo]
      properties:
        id: { type: integer, format: int64 }
        titulo: { type: string, maxLength: 120 }

# pom.xml (resumen)
# <plugin> org.openapitools:openapi-generator-maven-plugin
#   <inputSpec>src/main/resources/api/tareas.yaml</inputSpec>
#   <generatorName>spring</generatorName>
#   <configOptions><interfaceOnly>true</interfaceOnly><useSpringBoot3>true</useSpringBoot3></configOptions>
# -> genera TareasApi (interfaz) y Tarea (modelo); tu @RestController implementa TareasApi
```

- **En una línea:** `openapi-generator-cli generate -i api-spec.yaml -g spring -o ./generated --additional-properties=useSpringBoot3=true`
- **Por qué importa:** Permite que frontend y backend trabajen en paralelo: el frontend puede usar mocks generados automáticamente del contrato mientras el backend implementa la lógica real.
- **Error típico:** Desarrollar la API "code-first" y generar el contrato OpenAPI a posteriori, provocando que el contrato refleje las decisiones técnicas internas del backend en lugar de las necesidades del consumidor.
- **Java 17 frente a 21:** Independiente de versión.
- **Se conecta con:** OpenAPI 3.1, Swagger UI, Generación de Código, Trabajo en Paralelo

### 3. SpringDoc OpenAPI y Swagger UI en Spring Boot 3

La librería `springdoc-openapi-starter-webmvc-ui` genera automáticamente la documentación interactiva (Swagger UI) y el esquema OpenAPI 3.x a partir de las anotaciones `@RestController`, `@GetMapping`, `@Schema`, `@Operation`, etc. Disponible en `/swagger-ui.html` y `/v3/api-docs`.

**Cómo funciona por dentro.**

SpringDoc (`springdoc-openapi-starter-webmvc-ui` en su versión 2.x para Spring Boot 3) sigue el enfoque contrario, Code-First: analiza tus controladores, DTOs y anotaciones de validación en tiempo de ejecución y genera el documento OpenAPI en `/v3/api-docs`, con una interfaz Swagger UI en `/swagger-ui.html` para explorar y probar la API.

Sin hacer nada ya obtienes rutas, parámetros, cuerpos y esquemas. Con anotaciones como `@Operation`, `@ApiResponse` y `@Schema` añades descripciones, ejemplos y respuestas de error. Las restricciones de Bean Validation (`@NotBlank`, `@Size`) se reflejan en el esquema.

Es el camino más rápido para documentar una API existente. En producción conviene decidir si se expone la interfaz de Swagger o solo el JSON, y protegerla si la API no es pública.

*Documentar un endpoint con SpringDoc*

```java
// pom.xml: org.springdoc:springdoc-openapi-starter-webmvc-ui

@RestController
@RequestMapping("/api/tareas")
@Tag(name = "Tareas", description = "Gestión de tareas de un proyecto")
class TareaController {

    @Operation(summary = "Obtener una tarea por id")
    @ApiResponse(responseCode = "200", description = "Tarea encontrada")
    @ApiResponse(responseCode = "404", description = "No existe",
        content = @Content(schema = @Schema(implementation = ProblemDetail.class)))
    @GetMapping("/{id}")
    TareaResponse obtener(@Parameter(description = "Id de la tarea", example = "42")
                          @PathVariable long id) {
        return servicio.obtener(id);
    }
}

// application.yml
// springdoc:
//   api-docs.path: /v3/api-docs
//   swagger-ui.path: /swagger-ui.html
//   swagger-ui.enabled: ${SWAGGER_UI:false}     # desactivado salvo que se pida
```

- **En una línea:** `<dependency><groupId>org.springdoc</groupId><artifactId>springdoc-openapi-starter-webmvc-ui</artifactId><version>2.5.0</version></dependency>`
- **Por qué importa:** Proporciona documentación viva que siempre está sincronizada con el código real de la API, eliminando documentación obsoleta en wikis.
- **Error típico:** Usar la librería vieja `springfox` (abandonada) en vez de `springdoc-openapi` para Spring Boot 3.
- **Java 17 frente a 21:** springdoc 2.x es compatible con Spring Boot 3 / Java 17+.
- **Se conecta con:** Swagger UI, springdoc-openapi, Spring Boot 3

### 4. Filtros y Predicados en Spring Cloud Gateway

Los **Predicados** determinan si una petición coincide con una ruta (por Path, Host, Method, Header, Query Param). Los **Filtros** modifican la petición o la respuesta antes/después del enrutamiento: añadir headers, modificar el path, aplicar circuit breaker con Resilience4j, rate limiting con Redis, o autenticación JWT.

**Cómo funciona por dentro.**

Spring Cloud Gateway modela cada ruta con tres piezas: un destino (`uri`), predicados que deciden si una petición entra en la ruta (`Path`, `Method`, `Host`, `Header`, `Query`, `After`, `Weight` para despliegues canary) y filtros que modifican la petición antes de enviarla o la respuesta antes de devolverla (`StripPrefix`, `RewritePath`, `AddRequestHeader`, `Retry`, `CircuitBreaker`, `RequestRateLimiter`).

Los filtros de ruta se aplican solo a una ruta; los filtros globales (`GlobalFilter`) a todas. Un uso típico de filtro global es generar o propagar un id de correlación y registrar cada petición con su duración.

El gateway clásico está construido sobre WebFlux y Netty, así que los filtros propios deben ser no bloqueantes y devolver un `Mono<Void>`. Desde Spring Cloud 2023 existe además una variante sobre Spring MVC (Gateway Server MVC) para quien prefiera el modelo de Servlet.

*Filtro global de id de correlación*

```java
@Component
class CorrelacionFilter implements GlobalFilter, Ordered {

    private static final String CABECERA = "X-Correlation-Id";

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        String id = Optional.ofNullable(exchange.getRequest().getHeaders().getFirst(CABECERA))
            .orElseGet(() -> UUID.randomUUID().toString());

        ServerWebExchange conId = exchange.mutate()
            .request(r -> r.header(CABECERA, id))          // hacia el servicio
            .build();
        conId.getResponse().getHeaders().add(CABECERA, id); // hacia el cliente

        long inicio = System.nanoTime();
        return chain.filter(conId)
            .doFinally(s -> log.info("{} {} -> {} en {} ms [{}]",
                exchange.getRequest().getMethod(), exchange.getRequest().getPath(),
                conId.getResponse().getStatusCode(),
                (System.nanoTime() - inicio) / 1_000_000, id));
    }

    @Override
    public int getOrder() { return Ordered.HIGHEST_PRECEDENCE; }
}
```

- **En una línea:** `spring.cloud.gateway.routes[0].filters[0]=AddRequestHeader=X-Request-Source, gateway`
- **Por qué importa:** Permite aplicar patrones de resiliencia (circuit breaker, retry, rate limit) a nivel de gateway sin modificar los microservicios internos.
- **Error típico:** Configurar filtros pesados de transformación que aumentan la latencia del gateway.
- **Java 17 frente a 21:** Idéntico en 17 y 21.
- **Se conecta con:** Resilience4j, Redis Rate Limiter, JWT Validation

### 5. Versionado de APIs REST: Estrategias y Buenas Prácticas

Las APIs evolucionan con el tiempo y los contratos deben poder cambiar sin romper clientes existentes. Las estrategias principales son: (1) **Versionado por URL** (`/api/v1/pedidos`, `/api/v2/pedidos`), la más común y explícita; (2) **Versionado por header** (`Accept: application/vnd.empresa.v2+json`); (3) **Versionado por parámetro** (`?version=2`).

**Cómo funciona por dentro.**

La mejor estrategia de versionado es necesitarla poco: diseñar cambios compatibles hacia atrás. Añadir campos opcionales a una respuesta, nuevos endpoints o nuevos valores tolerados no rompe a clientes que ignoran lo que no conocen (configura Jackson para no fallar con propiedades desconocidas). Rompen: eliminar o renombrar campos, cambiar tipos, hacer obligatorio algo que era opcional o cambiar el significado de un código de estado.

Cuando un cambio incompatible es inevitable, hay tres formas habituales de versionar: en la URI (`/api/v2/tareas`, la más visible y fácil de enrutar en el gateway), en una cabecera (`X-API-Version: 2`) o en el tipo de medio (`Accept: application/vnd.acme.tarea.v2+json`). La de URI es la más usada por su sencillez. Spring Framework 7 (base de Spring Boot 4) añade soporte nativo para versionado.

Sea cual sea el método, publica un calendario de retirada: anuncia la obsolescencia con las cabeceras `Deprecation` y `Sunset`, mide quién sigue usando la versión antigua y retírala solo cuando el tráfico sea cero o se haya acordado.

*Dos versiones conviviendo con aviso de retirada*

```java
@RestController
@RequestMapping("/api/v1/tareas")
class TareaControllerV1 {
    @GetMapping("/{id}")
    ResponseEntity<TareaV1> obtener(@PathVariable long id) {
        return ResponseEntity.ok()
            .header("Deprecation", "@1767225600")                  // obsoleta desde 2026-01-01
            .header("Sunset", "Wed, 01 Jul 2026 00:00:00 GMT")     // se retira en esta fecha
            .header("Link", "</api/v2/tareas/" + id + ">; rel=\"successor-version\"")
            .body(TareaV1.desde(servicio.obtener(id)));
    }
}

@RestController
@RequestMapping("/api/v2/tareas")
class TareaControllerV2 {
    @GetMapping("/{id}")
    TareaV2 obtener(@PathVariable long id) {          // v2: "estado" sustituye al booleano "completada"
        return TareaV2.desde(servicio.obtener(id));
    }
}
```

- **En una línea:** `@RequestMapping("/api/v1/pedidos") vs @RequestMapping("/api/v2/pedidos")`
- **Por qué importa:** Permite desplegar nuevas versiones de la API sin romper la compatibilidad con clientes antiguos que aún consumen la versión anterior.
- **Error típico:** No versionar la API desde el día uno y después necesitar hacer cambios incompatibles sin poder mantener la versión antigua.
- **Java 17 frente a 21:** Independiente de versión.
- **Se conecta con:** Backward Compatibility, Deprecation, Semantic Versioning


## Sesión de 90 minutos

### Bloque 1: Diseño API-First con OpenAPI 3.1 (0 - 25 min)

- **Qué estudiar:** Estructura de un fichero openapi.yaml: info, paths, schemas, responses.
- **Pregunta a responder:** ¿Cómo definir un endpoint POST que devuelve 201 Created con un DTO en el body?
- **Entregable:** Fichero api-spec.yaml con 3 endpoints documentados.

### Bloque 2: Spring Cloud Gateway y Enrutamiento (25 - 50 min)

- **Qué estudiar:** Configuración de rutas con predicados de Path y filtros de headers.
- **Pregunta a responder:** ¿Cómo enrutar /api/v1/pedidos/** al servicio de pedidos y /api/v1/usuarios/** al servicio de usuarios?
- **Entregable:** application.yml con 3 rutas de gateway funcionales.

### Bloque 3: SpringDoc y Swagger UI (50 - 80 min)

- **Qué estudiar:** Integración de springdoc-openapi en Spring Boot 3 y anotaciones @Operation, @Schema.
- **Pregunta a responder:** ¿Cómo documentar errores de validación (400) y not found (404)?
- **Entregable:** Swagger UI accesible en /swagger-ui.html con documentación interactiva.

### Cierre y Consolidación (80 - 90 min)

- **Qué estudiar:** Versionado de APIs y criterios de decisión API-First vs Code-First.
- **Pregunta a responder:** ¿Cuándo es más práctico Code-First?
- **Entregable:** Notas de decisión en la app.


## Explícalo desde cero

### Por qué API-First ahorra semanas de trabajo en equipos frontend-backend

- **Qué entender:** Si el equipo frontend tiene que esperar a que el backend termine para saber la forma de los datos, todo el proyecto se ralentiza. Con API-First, el contrato YAML se firma antes de escribir código. El frontend genera mocks automáticos del YAML y construye su UI completa. El backend implementa contra las interfaces generadas. Cuando ambos terminan, se integran y funcionan a la primera.
- **Qué suele explicarse mal:** Pensar que API-First es "documentar la API con Swagger". Documentar a posteriori es Code-First; API-First significa que el YAML es el artefacto maestro que se diseña PRIMERO.
- **Ejemplo sencillo:** Construir una casa: el plano del arquitecto (contrato OpenAPI) se firma antes de que el fontanero (backend) y el electricista (frontend) empiecen a trabajar. Si cada uno trabaja sin plano, las tuberías y los cables no coincidirán.
- **Señal de alerta:** Si los endpoints de tu API devuelven campos que solo existen porque tu tabla de base de datos los tiene, en vez de devolver lo que el consumidor necesita.


## Preguntas

### Nivel 1 (Básico): ¿Por qué un API Gateway NO debe contener lógica de negocio?

El gateway debe ser un enrutador ligero sin estado. Si metes lógica de negocio, conviertes el gateway en un monolito centralizado, un punto único de fallo, difícil de escalar y de testear independientemente.

**Respuesta razonada:** Porque el gateway es compartido por todos los servicios y equipos. Si contiene lógica de negocio, cada cambio de negocio exige modificar y desplegar el gateway, que se convierte en un cuello de botella para todos los equipos y en un punto donde un error afecta a todo el sistema. Además, la lógica queda repartida entre el gateway y los servicios, rompiendo la autonomía y el encapsulamiento de cada uno: es el camino hacia un monolito distribuido.

Su responsabilidad son las preocupaciones transversales y técnicas: enrutar, autenticar, limitar, observar y proteger. Si un cliente concreto necesita agregar datos de varios servicios, la opción es un Backend for Frontend propio de ese cliente, no meter esa lógica en el gateway común.

### Nivel 2 (Intermedio): ¿Cuál es la diferencia entre API-First (Contract-First) y Code-First para generar documentación OpenAPI?

En API-First el contrato YAML/JSON se diseña y aprueba primero; el código se genera o implementa después contra ese contrato. En Code-First el desarrollador escribe el controlador Java y la documentación OpenAPI se genera automáticamente a partir de las anotaciones. API-First garantiza que la API está diseñada para el consumidor; Code-First suele reflejar la estructura interna del backend.

**Respuesta razonada:** En API-First se escribe primero el contrato OpenAPI, se acuerda con los consumidores y se genera el código del servidor (interfaces, DTOs) y de los clientes a partir de él. El contrato es la fuente de verdad: los cambios se discuten antes de implementarse y los equipos pueden trabajar en paralelo con mocks.

En Code-First se escribe el código y la documentación OpenAPI se genera a partir de él (por ejemplo con SpringDoc). Es más rápido para empezar y no hay ficheros que mantener sincronizados, pero el contrato es una consecuencia de la implementación y los cambios incompatibles pueden colarse sin que nadie los revise. API-First compensa en APIs públicas o consumidas por varios equipos; Code-First, en APIs internas de un solo equipo.

### Nivel 3 (Entrevista Senior): Spring Cloud Gateway se ejecuta sobre WebFlux/Netty. ¿Por qué no se puede usar Spring Data JPA (JDBC bloqueante) directamente dentro de un filtro del gateway?

Spring Cloud Gateway ejecuta sobre el Event Loop de Netty con pocos hilos. Una llamada JDBC bloqueante dentro de un filtro congelaría uno de esos escasos hilos del Event Loop, pudiendo bloquear todo el gateway. Si se necesita consultar una BD, se debe usar un cliente reactivo (R2DBC) o delegar a un microservicio downstream.

**Respuesta razonada:** Spring Cloud Gateway clásico se ejecuta sobre Netty con un número pequeño de hilos de event loop, que atienden todas las conexiones. JDBC y JPA son bloqueantes: mientras esperan a la base de datos, el hilo queda parado. Unas pocas peticiones lentas a la base de datos dentro de un filtro bastan para dejar todos los hilos bloqueados, y el gateway, por el que pasa todo el tráfico, deja de responder.

Si un filtro necesita datos (por ejemplo, permisos o límites por cliente), se usan clientes reactivos (R2DBC, Redis reactivo, `WebClient`), se cachean los datos en memoria o, como último recurso, se aísla la llamada en `Schedulers.boundedElastic()`. Y, sobre todo, se cuestiona si ese acceso a datos pertenece realmente al gateway.


## Reto del tutor

**Reto:** Tu empresa tiene 12 microservicios. Un equipo propone que cada microservicio exponga su propia documentación Swagger UI en su propio puerto. El equipo de frontend se queja porque debe consultar 12 URLs distintas. ¿Cómo centralizarías la documentación?

- **Lo que está bien:** Propone agregar la documentación en un punto central.
- **Respuesta superficial:** Decir "poner un Swagger UI en el gateway".
- **Lo que falta de fondo:** No saber que Spring Cloud Gateway puede agregar los spec JSON de cada microservicio usando la configuración de `springdoc.swagger-ui.urls` o un servicio de agregación de contratos.
- **Respuesta senior:** La solución es configurar el API Gateway con `springdoc-openapi-starter-webflux-ui` y usar la propiedad `springdoc.swagger-ui.urls` para agregar los esquemas `/v3/api-docs` de cada microservicio en un Swagger UI centralizado accesible desde una URL única del gateway. Adicionalmente, en producción se desactiva el acceso público a `/swagger-ui.html` con un filtro de seguridad o restricción de red.


## Katas

### Kata 26.1: Contrato OpenAPI 3.1 y Generación de Interfaces Spring

Escribe un fichero YAML openapi 3.1 con un endpoint GET /api/v1/productos que devuelva una lista de ProductoDto. Usa openapi-generator-cli para generar las interfaces Java y valida que compilan.


## Reglas de oro

- Diseña el contrato OpenAPI YAML antes de escribir código Java (API-First).
- El API Gateway es un enrutador sin estado; jamás pongas lógica de negocio allí.
- Versiona tus APIs desde el día 1 con /api/v1/ en la URL.
- Usa springdoc-openapi en Spring Boot 3, no springfox (abandonado).
- En producción, desactiva el acceso público a Swagger UI con restricciones de red o seguridad.


## Proyecto guiado «Pedidos»: Contrato OpenAPI y gateway

Definir la API antes de implementarla y poner una única puerta de entrada delante.

**Tareas:**

- Escribe el contrato OpenAPI 3.1 de la API de pedidos y genera las interfaces de los controladores.
- Pon Spring Cloud Gateway delante de `pedidos` y `notificaciones` con rutas por prefijo.
- Añade limitación de peticiones por cliente en el gateway.

**Está listo cuando:**

- Un cambio incompatible en el contrato rompe la compilación de los controladores.
- Superar el límite de peticiones devuelve 429 desde el gateway.


## Fuentes oficiales

- [Especificación OpenAPI 3.1](https://spec.openapis.org/oas/v3.1.0)
- [Spring Cloud Gateway: referencia](https://docs.spring.io/spring-cloud-gateway/reference/)
- [springdoc-openapi](https://springdoc.org/)

