Si nuestros endpoints devuelven String o Map, ya contamos con una API plenamente funcional, pero no es la mejor solución. El problema es que la estructura de la respuesta está demasiado abierta, lo que la hace más frágil y difícil de mantener.

Para especificar de forma clara el contrato de la API, es conveniente introducir en este punto los DTO (Data Transfer Objects).

El problema de devolver un Map

Hasta ahora, nuestra API devuelve datos, pero sin estar representados de forma explícita y mantenible. Spring serializa el Map sin problema a JSON, pero presenta una serie de inconvenientes que lo hacen poco adecuado para una API que empieza a evolucionar:

  1. No está tipado: No existe una estructura definida que indique qué campos debe contener la respuesta.
  2. Sin documentación clara: la forma del JSON no queda reflejada en ningún tipo o clase, dificultando entender qué devuelve realmente el endpoint.
  3. Fácil equivocarse en claves: las claves son cadenas de texto, por lo que un error tipográfico puede pasar desapercibido hasta tiempo de ejecución.
  4. Un cambio de nombre rompe contratos con clientes fácilmente: modificar una clave puede afectar a clientes que dependen de esa estructura sin que el compilador detecte el problema.
  5. Perdemos claridad y el apoyo del compilador durante el desarrollo: no contamos con autocompletado ni comprobaciones de tipos al trabajar con los datos.
  6. No es escalable: a medida que la respuesta crece, el Map se vuelve más difícil de leer, mantener y evolucionar.

El problema no es que falle, es que la estructura de la respuesta queda implícita, dispersa y demasiado abierta.

Qué es un DTO

Un DTO (Data Transfer Object) es un objeto cuyo objetivo es transportar datos entre capas o entre sistemas. En una API, uno de sus usos más habituales es representar de forma explícita los datos que enviamos o recibimos en los diferentes endpoints.

Son objetos simples cuya función es representar datos con una estructura clara y fija, sin contener ninguna lógica de negocio. De esta forma, la estructura de la respuesta queda definida de manera explícita y controlada.

Primer DTO en nuestra API

La respuesta que devolvemos actualmente ya está serializada en un JSON con el siguiente contenido:

{
    "message": "Hello Juan from Spring Boot!",
    "name": "Juan",
    "timestamp": "2026-04-04T11:30:38.578535500Z"
}

Spring la está formando a partir de un Map en el endpoint:

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);  
}

Un primer DTO para la respuesta de nuestro endpoint podría ser el siguiente, incluyendo un atributo por cada campo que queremos devolver:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public class HelloResponseDto {  
  
    private String message;  
    private String name;  
    private String timestamp;  
  
    public HelloResponseDto(String message, String name, String timestamp) {  
        this.message = message;  
        this.name = name;  
        this.timestamp = timestamp;  
    }  
  
    public String getMessage() {  
        return message;  
    }  
  
    public String getName() {  
        return name;  
    }  
  
    public String getTimestamp() {  
        return timestamp;  
    }  
  
}

Quedando el endpoint de la siguiente manera, sin alterar la respuesta de la petición:

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

Ahora la estructura de la respuesta queda definida de forma explícita en un tipo Java, facilitando su comprensión y evolución.

Usando record de Java

Como el objeto HelloResponseDto solo transporta datos, Java ofrece una forma más compacta de representarlo: record. Un record define datos inmutables, reduce el código repetitivo y encaja muy bien en objetos simples de transporte.

Podríamos redefinir la clase HelloResponseDto de la siguiente manera:

1
public record HelloResponseDto(String message, String name, String timestamp) {  }

El endpoint no necesita modificarse, ya que el record implementa automáticamente el constructor y los métodos de acceso.

Ventajas frente a Map

  1. Un Map describe datos de forma improvisada; un DTO describe datos de forma intencional.
  2. Más claridad: el nombre HelloResponseDto expresa qué estamos devolviendo.
  3. Estructura explícita: los campos están definidos en un tipo.
  4. Menos errores involuntarios en el desarrollo: no dependemos de escribir claves String a mano
  5. Mejor mantenimiento: el cambio está centralizado.
  6. Mejor evolución: más fácil añadir nuevos campos sin desordenar el controlador

Evolución de nuestra API

En el endpoint /hello comenzamos devolviendo un String, luego mejoramos la respuesta con ResponseEntity. Después pasamos a devolver un JSON. Y ahora damos un paso más definiendo explícitamente su estructura mediante DTOs. De esta forma, el contrato con los clientes se vuelve más robusto, mantenible y fiable, sentando además una base sólida para seguir evolucionando la API.