← Integraciones externas

Sesión 41 · Semana 21

Consumir un servicio externo

Proyecto compartido. En el taller de Intermodular que abre esta semana has trabajado publicar el acceso con jwt sin perder permisos. En Servidor continúas la implementación del mismo producto.

Se explica

25 minutos · explicación y demostración

Hasta ahora tu backend respondía a otros programas. Hoy también actuará como cliente de un servicio externo. RestClient envía peticiones HTTP desde Java; un DTO externo representa la respuesta del proveedor y un adaptador la convierte al formato propio de tu producto.

El backend deja de ser una isla solitaria

Hasta este punto del curso, tu aplicación backend siempre ha sido el servidor: esperaba pasivamente en el puerto 8080 a que un navegador o Bruno le enviaran peticiones HTTP para consultar la base de datos local de PostgreSQL.

En el desarrollo empresarial moderno ningún backend vive aislado:

  • Para enviar facturas electrónicas, consulta la API de Hacienda o de un proveedor tributario.
  • Para cobrar una suscripción, invoca la API de Stripe o PayPal.
  • Para planificar obras o tareas en exteriores en nuestro gestor de proyectos, necesita consultar un servicio meteorológico externo o verificar commits en la API de GitHub.
El backend actuando como cliente HTTP saliente
  1. Cliente Web / Móvil
  2. Backend Spring Boot (8080)
  3. Petición HTTP saliente
  4. API Externa (Open-Meteo / GitHub)

Clientes HTTP en Spring Boot: de RestTemplate a RestClient

En el ecosistema Java y Spring han existido tres generaciones de clientes HTTP:

Cliente HTTP Estado actual Estilo de programación Uso recomendado
RestTemplate En mantenimiento (desde Spring 5). Imperativo y rígido (getForObject, exchange). Aplicaciones legadas previas a Spring Boot 3.
WebClient Activo y potente. Reactivo no bloqueante (Project Reactor / WebFlux). Aplicaciones asíncronas con flujos reactivos de alta concurrencia.
RestClient El estándar moderno (Spring Boot 3.2+). Fluido, declarativo y síncrono. La opción recomendada para el 95 % de APIs REST empresariales en Spring MVC.

RestClient combina la sencillez síncrona de RestTemplate con la elegancia y expresividad de la interfaz fluida de WebClient, sin necesidad de arrastrar la complejidad reactiva de WebFlux.

El peligro mortal: Acoplar tu dominio a una API externa

En el trabajo anterior devolvimos un String con el JSON crudo de Open-Meteo. Aunque sirvió para ver que la red funcionaba, hacer eso en una aplicación real es un antipatrón arquitectónico gravísimo:

{
  "latitude": 39.4699,
  "longitude": -0.3763,
  "generationtime_ms": 0.04100799560546875,
  "utc_offset_seconds": 0,
  "timezone": "GMT",
  "timezone_abbreviation": "GMT",
  "elevation": 15.0,
  "current_weather": {
    "temperature": 22.4,
    "windspeed": 14.8,
    "winddirection": 180,
    "weathercode": 0,
    "is_day": 1,
    "time": "2026-09-02T12:00"
  }
}

Si este JSON se devuelve al cliente web o se almacena sin transformar en la base de datos:

  1. Tu frontend se acopla a las decisiones de un tercero: Si Open-Meteo cambia windspeed por wind_speed_kmh, tu pantalla de React/Angular deja de mostrar el viento.
  2. Contaminas tu arquitectura con ruido: A tu gestor de proyectos no le importa generationtime_ms ni utc_offset_seconds.
  3. Pérdida de semántica de negocio: El código weathercode: 0 es un número incomprensible; tu usuario necesita ver “Cielo despejado”.

El principio de la Capa Anticorrupción (ACL)

Ningún contrato externo debe cruzar la frontera de tu servicio de integración.

Los datos ajenos deben recibirse en DTOs Externos (propios del proveedor) y traducirse inmediatamente a Modelos Propios antes de entregarse a la capa de negocio.

La arquitectura de aislamiento

Aislamiento con Capa Anticorrupción (ACL)
  1. API Externa (Open-Meteo JSON)
  2. DTO Externo (OpenMeteoResponse)
  3. Adaptador / Mapeador
  4. DTO Interno del Dominio (ClimaProyectoResponse)
  5. Controlador / Frontend

Se trabaja

140 minutos · implementación guiada sobre el proyecto propio

Paso 1 · Retomar el proyecto y preparar la comprobación

  1. Abre una operación del servicio donde tenga sentido consultar información externa. El ejemplo meteorológico sirve de referencia; justifica su uso o el servicio equivalente en tu dominio.
  2. Localiza la configuración de la URL del proveedor y crea el paquete de integración separado de los controladores públicos.
  3. Consulta primero la respuesta externa de ejemplo y anota sus campos y unidades. Decide cuáles necesita realmente tu aplicación.

Paso 2 · La API externa de pruebas: Open-Meteo

Para aprender integración utilizaremos la API pública de Open-Meteo (open-meteo.com):

  • Es completamente gratuita y abierta para uso formativo y de desarrollo.
  • No requiere clave de API (API key): elimina barreras de registro y credenciales en las primeras prácticas.
  • Devuelve datos reales de previsión meteorológica a partir de coordenadas geográficas: https://api.open-meteo.com/v1/forecast?latitude=39.47&longitude=-0.38&current_weather=true

Paso 3 · Configuración y primer cliente con RestClient

Primero ejecuta la URL externa en el cliente HTTP y guarda una respuesta de ejemplo: es tu referencia para los nombres y tipos. Crea después config/RestClientConfig.java, integration/ClimaExternoClient.java y controller/ProyectoClimaController.java, cada bloque en su archivo. Arranca al completar sus dependencias y llama a la ruta de diagnóstico. Esa ruta muestra el transporte; todavía debes conectar la existencia y las coordenadas del proyecto en los pasos siguientes.

GET https://api.open-meteo.com/v1/forecast?latitude=39.47&longitude=-0.38&current_weather=true

Recibirás algo parecido a esto:

{
  "latitude": 39.5,
  "longitude": -0.375,
  "generationtime_ms": 0.0349,
  "utc_offset_seconds": 0,
  "timezone": "GMT",
  "elevation": 16.0,
  "current_weather": {
    "temperature": 18.4,
    "windspeed": 11.2,
    "winddirection": 91,
    "weathercode": 3,
    "is_day": 1,
    "time": "2026-03-12T09:00"
  }
}

Anota tres cosas, porque las tres condicionan todo lo que viene después:

  1. El proveedor decide los nombres. windspeed va en una palabra, current_weather en snake_case, weathercode es un número y no un texto. Tú no eliges nada de eso y puede cambiar sin avisarte.
  2. Viene mucho más de lo que necesitas. De ese objeto entero, a tu gestor de proyectos le interesan dos campos.
  3. Tarda. Fíjate en el tiempo que marca tu cliente HTTP: unas décimas de segundo. Comparado con los milisegundos de una consulta a tu PostgreSQL local, es una eternidad, y ese tiempo se lo vas a añadir a cada petición que lo use.

Configuramos un @Bean de RestClient en una clase de configuración:

package com.ejemplo.gestor.config;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.web.client.RestClient;

@Configuration
public class RestClientConfig {

    @Value("${app.integraciones.open-meteo.base-url:https://api.open-meteo.com}")
    private String openMeteoBaseUrl;

    @Bean
    public RestClient openMeteoRestClient() {
        return RestClient.builder()
            .baseUrl(openMeteoBaseUrl)
            .defaultHeader(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE)
            .defaultHeader(HttpHeaders.USER_AGENT, "GestorProyectosBackend/1.0 (formacion-dam)")
            .build();
    }
}

Creamos un servicio que efectúa la llamada saliente mediante la API fluida de RestClient:

package com.ejemplo.gestor.integration;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestClient;

@Service
public class ClimaExternoClient {

    private static final Logger log = LoggerFactory.getLogger(ClimaExternoClient.class);
    private final RestClient openMeteoRestClient;

    public ClimaExternoClient(RestClient openMeteoRestClient) {
        this.openMeteoRestClient = openMeteoRestClient;
    }

    public String obtenerClimaCrudo(double latitud, double longitud) {
        log.info("Iniciando petición HTTP saliente a Open-Meteo para lat={}, lon={}", latitud, longitud);
        long inicioMs = System.currentTimeMillis();

        String respuestaJson = openMeteoRestClient.get()
            .uri(uriBuilder -> uriBuilder
                .path("/v1/forecast")
                .queryParam("latitude", latitud)
                .queryParam("longitude", longitud)
                .queryParam("current_weather", true)
                .build())
            .retrieve()
            .body(String.class);

        long duracionMs = System.currentTimeMillis() - inicioMs;
        log.info("Respuesta recibida de Open-Meteo en {} ms. Longitud: {} caracteres", duracionMs, respuestaJson.length());

        return respuestaJson;
    }
}
package com.ejemplo.gestor.controller;

import com.ejemplo.gestor.integration.ClimaExternoClient;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/api/v1/proyectos")
public class ProyectoClimaController {

    private final ClimaExternoClient climaExternoClient;

    public ProyectoClimaController(ClimaExternoClient climaExternoClient) {
        this.climaExternoClient = climaExternoClient;
    }

    @GetMapping(value = "/{id}/clima-raw", produces = MediaType.APPLICATION_JSON_VALUE)
    public ResponseEntity<String> consultarClimaRaw(
            @PathVariable Long id,
            @RequestParam(defaultValue = "39.4699") double lat,
            @RequestParam(defaultValue = "-0.3763") double lon) {

        String jsonCrudo = climaExternoClient.obtenerClimaCrudo(lat, lon);
        return ResponseEntity.ok(jsonCrudo);
    }
}
Por qué el RestClient es un @Bean y no un new
Por lo mismo por lo que en la UD4 dejaste de hacer new TareaRepositorio(): el destino, las cabeceras y —en la sesión 42— los timeouts son configuración, y la configuración se declara una vez en un sitio y se inyecta. Además te permitirá sustituirlo por un doble en los tests sin tocar el servicio.
Por qué de momento devolvemos String
Es deliberado y dura una sola sesión. En esta sesión interesa observar el JSON externo en su forma original, con todos sus campos y su nomenclatura propia. En la sesión 41 ese String se convierte en un DTO propio, y entenderás la diferencia mucho mejor habiendo visto antes el volcado crudo.
.retrieve().body(...)
retrieve() ejecuta la petición y body() deserializa la respuesta al tipo que le pidas. Con String no deserializa nada: te entrega el texto. Ojo, retrieve() lanza excepción ante un 4xx o 5xx remoto, y eso hoy todavía no lo estamos tratando: es justo el tema de la sesión 42.
La cabecera User-Agent
No es decorativa. Muchos proveedores rechazan o limitan peticiones anónimas, y algunos (GitHub, sin ir más lejos) devuelven 403 si no la envías. Identificar tu cliente es una cortesía que además evita bloqueos.

Paso 4 · Comprobar parámetros y respuesta de la integración HTTP

  1. Arranca la aplicación Spring Boot.

  2. Abre Bruno y lanza: GET http://localhost:8080/api/v1/proyectos/1/clima-raw?lat=39.4699&lon=-0.3763

  3. Observa la respuesta: Recibes un JSON real emitido por los servidores de Open-Meteo con temperatura, velocidad de viento y código del tiempo.

  4. Inspecciona la consola de Spring Boot:

    INFO : Iniciando petición HTTP saliente a Open-Meteo para lat=39.4699, lon=-0.3763
    INFO : Respuesta recibida de Open-Meteo en 214 ms. Longitud: 382 caracteres

    Comprueba cómo tu backend tardó más de 200 ms: ese tiempo no fue CPU local, fue el tiempo que tardó el paquete IP en viajar por Internet, cruzar routers, ser procesado por el proveedor remoto y volver.

  5. Mide el coste de la integración: lanza GET /api/v1/proyectos/1 (el endpoint normal, sin clima) y compara el tiempo que marca tu cliente HTTP con el de /clima-raw. La diferencia es lo que cuesta salir a Internet, y es el número que justifica toda la sesión 42.

Paso 5 · Si algo no sale como dice el guion

Síntoma Causa casi segura Qué mirar
UnknownHostException: api.open-meteo.com No hay salida a Internet Proxy del centro o firewall. Comprueba primero que la URL del paso 1 funciona en el navegador
404 Not Found desde Open-Meteo La ruta está duplicada o incompleta Si el baseUrl ya trae https://api.open-meteo.com, el path debe ser /v1/forecast, ni /forecast ni la URL entera
Parameter 'openMeteoRestClient' not found Hay más de un bean RestClient Inyecta por nombre exacto, o marca uno con @Qualifier
La respuesta llega vacía o null Faltan parámetros obligatorios Open-Meteo exige latitude y longitude; sin current_weather=true no devuelve el bloque que buscas
Tarda muchísimo y acaba colgado No hay timeout configurado Es correcto: todavía no lo has puesto. Ese es exactamente el problema de la sesión 42

Paso 6 · Parametrizar la ubicación de la sede del proyecto

En lugar de pasar las coordenadas por parámetros de query en cada llamada:

  1. Añade a tu entidad Proyecto dos campos persistentes: latitud (Double) y longitud (Double). Recuerda que con ddl-auto=update Hibernate añade las columnas solo, pero las filas que ya existían quedan a null: actualízalas con un UPDATE a mano o dales valor por defecto.
  2. Modifica el endpoint para que consulte el proyecto en base de datos (ProyectoRepository.findById(id)) y utilice automáticamente sus coordenadas geográficas reales.
  3. Decide qué debe pasar si un proyecto no tiene coordenadas. No hay respuesta única, pero sí una mala: reventar con un NullPointerException. Elige entre devolver 400 explicando que ese proyecto no tiene sede geográfica, o no llamar a Open-Meteo y devolver el proyecto sin clima. Escribe en tu cuaderno cuál eliges y por qué.
  4. Prueba con dos proyectos distintos: uno en Valencia (39.47, -0.38) y otro en Madrid (40.41, -3.70), verificando que cada uno devuelve el tiempo de su propia ubicación, y un tercero sin coordenadas para comprobar la decisión del punto 3.
  5. Añade las tres peticiones a una carpeta 10-integraciones de tu colección.
Cómo saber que lo has terminado
Dos proyectos con coordenadas distintas devuelven temperaturas distintas; el proyecto sin coordenadas responde lo que tú decidiste y no un 500; y en los logs aparece una línea de inicio y una de fin con los milisegundos reales de cada llamada saliente.

Cliente HTTP y DTO externos

Paso 7 · De DTOs externos al modelo de dominio

Crea cada record externo en su propio archivo bajo integration/dto; la anotación JsonProperty relaciona una clave del proveedor con un componente Java de nombre distinto. Crea luego el DTO público en dto, el adaptador y el servicio que lo utiliza. En el controlador existente sustituye la llamada que devuelve JSON crudo por la llamada al nuevo servicio y cambia el tipo de respuesta. Comprueba que nombres, unidades y valores del DTO proceden del ejemplo externo guardado, no de constantes del guion.

package com.ejemplo.gestor.integration.dto;

import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;

@JsonIgnoreProperties(ignoreUnknown = true)
public record OpenMeteoResponse(
    double latitude,
    double longitude,
    @JsonProperty("current_weather") CurrentWeatherExternal current
) {}

El objeto anidado CurrentWeatherExternal se declara así:

package com.ejemplo.gestor.integration.dto;

import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;

@JsonIgnoreProperties(ignoreUnknown = true)
public record CurrentWeatherExternal(
    double temperature,
    double windspeed,
    @JsonProperty("weathercode") int weatherCode,
    @JsonProperty("is_day") int isDay,
    String time
) {}
@JsonIgnoreProperties(ignoreUnknown = true), la anotación que evita que te rompan la aplicación desde fuera
Sin ella, Jackson lanza UnrecognizedPropertyException en cuanto el JSON trae un campo que el record no declara, y la incorporación de ese campo depende del proveedor, sin notificación previa. Con ella, tu integración sobrevive a que Open-Meteo publique diez campos nuevos mañana.
@JsonProperty: dónde muere el snake_case ajeno
current_weather no es un nombre válido en tu código Java. @JsonProperty("current_weather") hace la traducción una sola vez, en la frontera. A partir de ahí, dentro de tu aplicación, el campo se llama current y nadie tiene que recordar cómo lo llamaba el proveedor.
Por qué estos DTO viven en integration.dto y no en dto
Porque el paquete es documentación. Cualquiera que abra integration.dto sabe que lo de dentro no lo decides tú y que puede cambiar sin previo aviso. Si mezclas esas clases con las tuyas, en seis meses nadie sabrá cuáles se pueden refactorizar con libertad y cuáles están atadas a un contrato ajeno.

Este es el contrato que le pertenece a nuestra aplicación: nombres limpios, unidades explícitas y descripción humana:

package com.ejemplo.gestor.dto;

public record ClimaProyectoResponse(
    Double temperaturaCelsius,
    Double velocidadVientoKmH,
    String descripcionClima,
    Boolean esFavorableParaTrabajoExterior
) {}

El adaptador interpreta los códigos numéricos del proveedor y genera nuestra regla de negocio:

package com.ejemplo.gestor.integration;

import com.ejemplo.gestor.dto.ClimaProyectoResponse;
import com.ejemplo.gestor.integration.dto.OpenMeteoResponse;
import org.springframework.stereotype.Component;

@Component
public class ClimaAdapter {

    public ClimaProyectoResponse adaptar(OpenMeteoResponse external) {
        if (external == null || external.current() == null) {
            return null;
        }

        var current = external.current();
        String descripcion = descifrarCodigoMeteorologico(current.weatherCode());

        // Regla de negocio propia: si el viento supera 40 km/h o hay tormenta/lluvia intensa, no es favorable
        boolean esFavorable = current.windspeed() < 40.0 && current.weatherCode() < 50;

        return new ClimaProyectoResponse(
            current.temperature(),
            current.windspeed(),
            descripcion,
            esFavorable
        );
    }

    private String descifrarCodigoMeteorologico(int code) {
        return switch (code) {
            case 0 -> "Cielo despejado";
            case 1, 2, 3 -> "Parcialmente nublado";
            case 45, 48 -> "Niebla";
            case 51, 53, 55 -> "Llovizna";
            case 61, 63, 65 -> "Lluvia";
            case 71, 73, 75 -> "Nieve";
            case 95, 96, 99 -> "Tormenta eléctrica";
            default -> "Condiciones variables (código " + code + ")";
        };
    }
}

Modificamos el cliente para que deserialice directamente al DTO externo y devuelva el modelo interno mediante el adaptador:

package com.ejemplo.gestor.integration;

import com.ejemplo.gestor.dto.ClimaProyectoResponse;
import com.ejemplo.gestor.integration.dto.OpenMeteoResponse;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestClient;

@Service
public class ClimaService {

    private final RestClient openMeteoRestClient;
    private final ClimaAdapter climaAdapter;

    public ClimaService(RestClient openMeteoRestClient, ClimaAdapter climaAdapter) {
        this.openMeteoRestClient = openMeteoRestClient;
        this.climaAdapter = climaAdapter;
    }

    public ClimaProyectoResponse consultarClima(double latitud, double longitud) {
        OpenMeteoResponse respuestaExterna = openMeteoRestClient.get()
            .uri(uriBuilder -> uriBuilder
                .path("/v1/forecast")
                .queryParam("latitude", latitud)
                .queryParam("longitude", longitud)
                .queryParam("current_weather", true)
                .build())
            .retrieve()
            .body(OpenMeteoResponse.class); // Deserialización automática con Jackson

        return climaAdapter.adaptar(respuestaExterna);
    }
}

Paso 8 · El contrato limpio en Bruno

Actualiza tu controlador para devolver ClimaProyectoResponse y lanza la petición en Bruno:

GET http://localhost:8080/api/v1/proyectos/1/clima

Respuesta recibida:

{
  "temperaturaCelsius": 23.1,
  "velocidadVientoKmH": 11.4,
  "descripcionClima": "Cielo despejado",
  "esFavorableParaTrabajoExterior": true
}

Comprueba la diferencia:

  • Ningún campo en inglés extraño del proveedor.
  • Cero metadatos inútiles de husos horarios o tiempos de CPU de Open-Meteo.
  • Añadido valor de negocio real (esFavorableParaTrabajoExterior).
  • Inmunidad garantizada: Si Open-Meteo decide añadir 10 campos nuevos mañana, Jackson los ignorará en silencio y tu aplicación seguirá funcionando sin tocar ni una línea.

La inmunidad de la que habla el punto anterior no es una promesa: se comprueba en dos minutos.

  1. Abre CurrentWeatherExternal y borra el componente isDay.
  2. Vuelve a lanzar la petición. Sigue funcionando: Jackson descarta el campo que ya no declaras.
  3. Ahora, en OpenMeteoResponse, quita la anotación @JsonIgnoreProperties(ignoreUnknown = true).
  4. Lanza otra vez. Ahora falla, con UnrecognizedPropertyException: Unrecognized field "generationtime_ms".
  5. Devuelve la anotación a su sitio.

Acabas de ver, en tu propia aplicación, cómo un campo que a ti no te importa —y que ni siquiera pediste— puede tumbar tu backend. Esa línea es lo único que separa una integración robusta de una que se cae el día que el proveedor despliega.

Paso 9 · Si algo no sale como dice el guion

Síntoma Causa casi segura Qué mirar
Todos los campos llegan a 0.0 o null Los nombres no coinciden El JSON dice windspeed, en una palabra. Compara letra a letra con el volcado del paso 1 de la sesión 41
UnrecognizedPropertyException Falta la anotación @JsonIgnoreProperties(ignoreUnknown = true) en cada record externo, también en los anidados
current llega null y peta el adaptador Falta el @JsonProperty del anidado current_weather no se mapea solo a current
Cannot construct instance ... no Creators Estás usando una clase, no un record Con record, Jackson usa el constructor canónico. Con una clase necesitarías constructor vacío y setters
El adaptador devuelve null y el controlador lanza NullPointerException El null del adaptador no lo trata nadie Es una decisión pendiente: la resuelve la sesión 42 con la degradación elegante

Paso 10 · Integrar el clima en la respuesta completa del proyecto

Antes de ampliar ProyectoResponse, decide dónde se incluye el clima: aquí se consulta en el detalle, no durante cada conversión genérica del mapper. Añade el componente y actualiza sus constructores en código y tests. Carga primero el proyecto y sus coordenadas; si faltan, devuelve una respuesta prevista por tu contrato, no una llamada con valores inventados. Consulta después el proveedor y construye el DTO final. Comprueba que listar cien proyectos no provoca cien llamadas meteorológicas por reutilizar ese mapper.

  1. Añade un campo opcional ClimaProyectoResponse clima.
  2. En ProyectoService.obtenerPorId(id), llama a climaService.consultarClima(proyecto.getLatitud(), proyecto.getLongitud()) e incrusta el clima en la respuesta.
  3. Verifica que al consultar los detalles de un proyecto, la respuesta contiene tanto los datos de la base de datos local (nombre, cliente, tareas) como el clima en tiempo real de su ubicación.
  4. Amplía la regla de negocio del adaptador: añade a ClimaProyectoResponse un campo String recomendacion que devuelva "Aplazar trabajo en exterior" cuando no sea favorable y "Condiciones adecuadas" cuando sí lo sea. Fíjate en dónde estás poniendo esa regla: en tu adaptador, no en el DTO externo. Open-Meteo no sabe nada de obras.
  5. Repasa la sesión 30: GET /api/v1/proyectos devuelve una lista paginada. Si incrustas el clima también ahí, una página de 20 proyectos dispara 20 llamadas a Internet y tarda cuatro segundos. Decide qué haces —incrustarlo solo en el detalle, o solo cuando se pida con ?incluirClima=true— y anótalo con su justificación. Es el mismo razonamiento del N+1 de la UD5, pero contra una red en lugar de contra una base de datos.
  6. Actualiza la documentación OpenAPI de la UD7: el nuevo campo clima necesita su @Schema con ejemplo, y el endpoint debe declarar que ese campo puede venir vacío.
Cómo saber que lo has terminado
El JSON que devuelve tu API no contiene ni un solo nombre de campo de Open-Meteo; ningún OpenMeteoResponse sale del paquete integration; has decidido y justificado qué pasa con el listado paginado; y borrar un campo del DTO externo no rompe nada.

Paso 11 · Comprobar y registrar el resultado del proyecto

  1. Ejecuta la consulta a través de tu backend y comprueba que el DTO público usa nombres y unidades propios, sin reenviar toda la respuesta del proveedor.
  2. Cambia un parámetro permitido y verifica que llega correctamente al proveedor. Los errores y reglas del contrato público siguen siendo responsabilidad de tu API.

Ampliación si has completado el trabajo

Primero termina y verifica los pasos anteriores. Estos retos profundizan en el mismo contenido; no sustituyen la entrega ni obligan a iniciar otro proyecto.

Reto · Consumir la API pública de GitHub para inspeccionar repositorios

Muchos proyectos de software tienen un repositorio de código asociado.

  1. Investiga la API pública de GitHub para consultar un repositorio público: GET https://api.github.com/repos/{propietario}/{repositorio}
  2. Configura un segundo cliente githubRestClient en RestClientConfig añadiendo la cabecera obligatoria User-Agent.
  3. Implementa un método que consulte un repositorio (por ejemplo, spring-projects/spring-boot) y devuelva el número de estrellas (stargazers_count) y si está archivado (archived).
Objetivo mínimoBean RestClient configurado y llamada funcional a Open-Meteo recuperando el JSON de respuesta.
Si lo tienesCoordenadas vinculadas a la entidad Proyecto y tiempo de latencia registrado en logs.
RetoSegundo cliente HTTP integrado consultando la API de repositorios de GitHub con cabeceras requeridas.
Ver respuestas

1 · Para ofrecer una interfaz fluida, moderna y síncrona que sustituya al viejo RestTemplate sin obligar al desarrollador a incorporar la complejidad y dependencias reactivas de WebFlux/WebClient.

2 · El método restClient.get().

3 · Porque muchos servidores y firewalls externos (como GitHub o Cloudflare) rechazan peticiones sin User-Agent para prevenir abusos de bots anónimos.

4 · La latencia de propagación física de la red en Internet, la resolución DNS y la negociación criptográfica TLS (handshake HTTPS).

Reto · Pruebas unitarias del Adaptador sin llamadas de red

Una de las enormes ventajas de la Capa Anticorrupción es que el mapeador puede probarse al 100 % sin levantar la red ni llamar a Internet:

  1. Crea una clase de test ClimaAdapterTest.
  2. Instancia objetos OpenMeteoResponse simulados con diferentes códigos (ej: código 0, código 63, código 95).
  3. Verifica mediante aserciones de JUnit que:
    • El código 0 traduce a "Cielo despejado" y esFavorableParaTrabajoExterior es true.
    • Un viento de 55 km/h o un código 95 (tormenta) marca esFavorableParaTrabajoExterior en false.
Objetivo mínimoDTOs externos anotados con @JsonIgnoreProperties y deserialización automática con Jackson.
Si lo tienesAdaptador ClimaAdapter desacoplando el modelo ajeno y traduciendo a ClimaProyectoResponse.
RetoSuite de pruebas unitarias sobre el adaptador validando reglas de negocio climáticas sin red.
Ver respuestas

1 · Porque cualquier cambio o deprecación en la API del proveedor externo rompería de forma involuntaria el contrato de tu propio frontend.

2 · Mapea el nombre del campo en el JSON entrante con el nombre del atributo o parámetro en la clase Java cuando no coinciden exactamente.

3 · Lanzaría una excepción de tipo UnrecognizedPropertyException y la petición fallaría con error 500.

4 · En el adaptador o en un servicio de dominio de nuestra aplicación, nunca en los DTOs externos ni en el proveedor remoto.

Cierre

15 minutos · resultado comprobable y explicación individual

Al terminar la sesión:

El dominio no depende directamente del formato externo y las credenciales no aparecen en el repositorio.

Cada integrante explica una decisión del código apoyándose en una de las comprobaciones realizadas.