Hasta ahora nuestro código está devolviendo un String de forma directa que Spring Boot convierte automáticamente en una respuesta HTTP completa. Ésta, por defecto, incluye un código de estado (200 OK) y un cuerpo con el mensaje.

El siguiente paso para mejorar el diseño de nuestra API es controlar completamente la respuesta y adaptarla a nuestra lógica, para lo que contamos con la clase ResponseEntity.

Qué es una respuesta HTTP

Una respuesta HTTP es el mensaje que devuelve el servidor cuando un cliente realiza una petición a una API. Esta respuesta incluye principalmente un código de estado, que indica si la operación ha sido correcta o ha ocurrido algún problema, y un cuerpo con los datos que queremos devolver.

200 OK
Content-Type: application/json

{ "Hello World from Spring Boot!" }

En nuestro caso:

  • 200 OK: la petición se ha procesado correctamente.
  • Content-Type: indica el tipo de contenido, JSON en este caso.
  • Hello World from Spring Boot!: cuerpo de la respuesta, un String que Spring Boot serializa automáticamente.

Este concepto es importante porque, a medida que nuestra API evolucione, no solo devolveremos texto, sino también distintos códigos HTTP y estructuras más completas, y ahí es donde entra ResponseEntity.

Controlando la respuesta con ResponseEntity

ResponseEntity es una clase de Spring que permite controlar completamente la respuesta HTTP que devuelve nuestra API. En lugar de devolver solo el contenido, podemos definir también el código de estado, las cabeceras y el cuerpo de la respuesta.

Hasta ahora, nuestro controlador devuelve un String directamente:

1
2
3
4
@GetMapping("/hello")  
public String hello(@RequestParam(defaultValue = "World") String name) {  
    return "Hello " + name + " from Spring Boot!";  
}

Podemos usar ResponseEntity para indicar explícitamente el código HTTP que debe incluir:

1
2
3
4
5
@GetMapping("/hello")  
public ResponseEntity<String> hello(@RequestParam(defaultValue = "World") String name) {  
    String content = "Hello " + name + " from Spring Boot!";  
    return ResponseEntity.ok(content);  
}

En este caso seguimos devolviendo el mismo contenido, pero ahora estamos construyendo explícitamente una respuesta HTTP, algo fundamental cuando queramos devolver errores (códigos 400 o 404), recursos creados ( código 201) o respuestas más complejas.

Completando la respuesta con un JSON

Otra mejora para conseguir una respuesta más adecuada para una API es devolver el contenido como un objeto JSON bien estructurado.

1
2
3
4
5
6
7
@GetMapping("/hello")  
public ResponseEntity<Map<String, String>> hello(@RequestParam(defaultValue = "World") String name) {  
    Map<String, String> content = Map.of(  
            "message", "Hello " + name + " from Spring Boot!"  
    );  
    return ResponseEntity.ok(content);  
}

En el caso de un String simple puede parecer de poco interés, pero lo habitual será que la respuesta incluya mucha más información, de forma que un JSON permita estructurar mejor los datos.

1
2
3
4
5
6
7
8
9
@GetMapping("/hello")  
public ResponseEntity<Map<String, String>> hello(@RequestParam(defaultValue = "World") String name) {  
    Map<String, String> content = Map.of(  
            "message", "Hello " + name + " from Spring Boot!",  
            "name", name,  
            "timestamp", Instant.now().toString()  
    );  
    return ResponseEntity.ok(content);  
}

Si levantamos el servidor y, como venimos haciendo, realizamos una petición desde el navegador, obtendremos la respuesta con el JSON completo.

Pantalla del navegador con la respuesta JSON de la API

Validando la API con los test

Una vez más, debemos usar los test para validar los cambios realizados. Hasta ahora, el contrato de nuestra API incluía una respuesta HTTP formada automáticamente con un único String en el cuerpo. Pero ahora, la respuesta es mucho más completa, con otro formato y con el mensaje dentro de un JSON.

Si ejecutamos los test existentes, éstos fallarán, indicando que hemos roto ese contrato.

Cambio de contrato con los clientes

Si nuestra API se encontrase en producción, esto nos indicaría que tendríamos un problema que podríamos afrontar con dos planteamientos:

  • Obligar a todos nuestros clientes a actualizar sus desarrollos para adaptarse al nuevo formato de respuesta.
  • Versionar nuestra API, manteniendo el endpoint actual sin modificar y publicar el nuevo en una nueva ruta, permitiendo a los clientes migrar de forma progresiva.

Evolucionando los tests

Para validar el nuevo desarrollo, adaptamos los test a la nueva respuesta del endpoint.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
@Test  
void helloWithoutParamReturnsDefaultValue() throws Exception {  
    mockMvc.perform( get("/hello") )  
            .andExpect( status().isOk() )  
            .andExpect( jsonPath("$.message").value("Hello World from Spring Boot!") )  
            .andExpect( jsonPath("$.name").value("World") )  
            .andExpect( jsonPath("$.timestamp").isString() );  
}  
  
@Test  
void helloWithParamReturnsCustomName() throws Exception {  
    mockMvc.perform(get("/hello").param("name", "Juan"))  
            .andExpect(status().isOk())  
            .andExpect(jsonPath("$.message").value("Hello Juan from Spring Boot!"))  
            .andExpect(jsonPath("$.name").value("Juan"))  
            .andExpect(jsonPath("$.timestamp").isString());  
}

jsonPath("$.message") indica a MockMvc que acceda al campo message del JSON devuelto:

  • $: representa la raíz del JSON
  • $.message: accede al campo message
  • .value(...): comprueba que ese campo tiene el valor esperado

Por lo tanto, permite validar un campo concreto dentro del JSON que devuelve la API y verifica que:

  • La respuesta tiene formato JSON
  • Existe el campo message
  • Su valor es exactamente el esperado en cada caso

Este enfoque permite validar respuestas más complejas de forma precisa, comprobando cada campo individualmente en lugar de comparar todo el contenido como una cadena de texto, lo que hace los tests más robustos y fáciles de mantener.

Conclusiones

Mejorando la respuesta de nuestra API, pasando de devolver texto simple a diseñar respuestas HTTP más completas, estamos evolucionándola de forma natural para conseguir un resultado más profesional. Además, con el trabajo continuo con los test, conseguimos validar el contrato que establecemos con nuestros clientes, de forma que podamos realizar esa evolución con seguridad.