Día 13 de 30 Fase 3: El Salto a Spring Boot 3 y APIs REST

APIs REST Robustas: DTOs con Records y ProblemDetail RFC 7807

@RestController, Jakarta Validation Declarativa y Manejo Global de Errores en 17/21

Para dominar. Al terminar deberías poder hacerlo sin ayuda.

  • 4h de estudio
  • 5 conceptos
  • 3 preguntas
  • 1 katas
Java 17 LTSJava 21 LTS@RestControllerDTOsJakarta ValidationProblemDetailRFC 7807
Al terminar este día sabrás 4
  • Diseñar el contrato de una API con DTOs de entrada y salida separados de las entidades.
  • Devolver errores en formato estándar ProblemDetail desde un único punto.
  • Validar peticiones de forma declarativa y responder con los errores por campo.
  • Elegir el método y el código de estado HTTP correctos para cada operación.

DTOs Inmutables con Records: Jamás Exponer Entidades en la API

Las peticiones y respuestas REST deben ser record dedicados. Exponer entidades de BD provoca fugas de seguridad, ciclos de serialización JSON y acoplamiento severo.

En una línea public record CrearTareaRequest(@NotBlank String titulo) {}

Contrato de entrada y salida con records java
public record CrearTareaRequest(
        @NotBlank @Size(max = 120) String titulo,
        @Size(max = 2000) String descripcion,
        @Min(1) @Max(10) int prioridad) {}

public record TareaResponse(
        long id,
        String titulo,
        String descripcion,
        int prioridad,
        boolean completada,
        Instant creadaEn) {

    static TareaResponse desde(Tarea t) {
        return new TareaResponse(t.getId(), t.getTitulo(), t.getDescripcion(),
                t.getPrioridad(), t.isCompletada(), t.getCreadaEn());
    }
}

@RestController
@RequestMapping("/api/tareas")
class TareaController {
    private final TareaService servicio;
    TareaController(TareaService servicio) { this.servicio = servicio; }

    @GetMapping("/{id}")
    TareaResponse obtener(@PathVariable long id) {
        return TareaResponse.desde(servicio.obtener(id));
    }
}
Cómo funciona por dentro

Un DTO (Data Transfer Object) define la forma exacta de los datos que entran y salen por la API. Separarlo de la entidad JPA evita cuatro problemas: exponer campos internos (contraseñas, flags de auditoría), permitir que el cliente modifique campos que no debería (over-posting), acoplar el contrato público al esquema de la base de datos y los ciclos o cargas perezosas al serializar relaciones.

Lo habitual es tener DTOs distintos para la entrada y la salida: la petición de creación no lleva id ni fechas generadas, la respuesta sí. Los records son perfectos para ello: inmutables, concisos y compatibles con Jackson y con Bean Validation.

El mapeo entre entidad y DTO puede ser un método estático, un mapper dedicado o una librería como MapStruct. Lo importante es que viva en la capa de aplicación o de adaptadores, no dentro de la entidad.

Aísla la API pública de la estructura de las tablas de base de datos.

Se conecta con RecordsSeguridad de APIsClean Architecture

Cuatro bloques cronometrados. No pases al siguiente sin tener el resultado esperado.

  1. Bloque 1: Controladores y DTOs con Records (0 - 25 min)

    Qué estudiar. Anotaciones @RestController, @RequestMapping, @GetMapping, @PostMapping.

    Pregunta a responder. ¿Por qué los DTOs deben ser records inmutables?

    Resultado esperado. Endpoints GET y POST para un recurso Tarea usando DTOs Records.

  2. Bloque 2: Jakarta Validation (25 - 50 min)

    Qué estudiar. Uso de @Valid, @NotBlank, @Size y validaciones complejas.

    Pregunta a responder. ¿Qué excepción lanza Spring cuando falla @Valid en el cuerpo JSON?

    Resultado esperado. DTO con validaciones probadas enviando JSON inválido.

  3. Bloque 3: RFC 7807 ProblemDetail (50 - 80 min)

    Qué estudiar. Clase ProblemDetail y @RestControllerAdvice.

    Pregunta a responder. ¿Cómo agregar propiedades personalizadas (e.g. timestamp, errores de campo) a un ProblemDetail?

    Resultado esperado. Manejador global que devuelve ProblemDetail ante errores 404 y 400.

  4. Cierre y Verificación (80 - 90 min)

    Qué estudiar. Prueba de endpoints con fichero .http o curl verificando cabeceras.

    Pregunta a responder. Verificar que POST devuelve 201 y cabecera Location.

    Resultado esperado. Progreso guardado en la base de datos de estudio.

Tres formas de comprobar si lo has entendido, de menos a más exigente: explicarlo con tus palabras, responder preguntas trampa y resolver el reto del tutor.

Explícalo desde cero

Lee cada ficha y explícala en voz alta antes de seguir: si puedes contárselo a alguien que empieza, lo has entendido.

Por qué ProblemDetail (RFC 7807) es un salto gigante frente a versiones anteriores

Lo que debes entender

Antes de Spring Boot 3, cada empresa inventaba su propio formato de error (CustomErrorResponse). Si consumías 5 microservicios distintos, tenías que parsear 5 formatos de error diferentes. RFC 7807 es un estándar IETF internacional unificado.

Analogía para contarlo

El enchufe eléctrico estándar europeo frente a que cada fabricante de electrodomésticos te obligue a usar una clavija diferente con forma triangular o hexagonal.

Lo que suele explicarse mal

Explicarlo como "otra clase más de Spring", sin mencionar que es un estándar oficial de internet (RFC).

Señal de que aún no lo dominas

Si en una app Spring Boot 3 devuelves un Map<String, Object> en tu manejador de excepciones.

Preguntas trampa

Responde de memoria, en voz alta o por escrito, y después abre la respuesta razonada: lo importante es el porqué, no la palabra exacta.

Nivel 1 (Básico) ¿Cuál es la diferencia entre @NotNull, @NotEmpty y @NotBlank en Jakarta Validation? Ver respuesta

Respuesta razonada

Las tres rechazan null, pero difieren en qué más aceptan. @NotNull solo exige que no sea null: "" y " " son válidos. @NotEmpty exige no nulo y longitud o tamaño mayor que cero; vale para String, colecciones, mapas y arrays, y acepta " ". @NotBlank solo vale para cadenas y exige al menos un carácter que no sea espacio en blanco.

Regla práctica: @NotBlank para textos obligatorios como nombres o títulos, @NotEmpty para listas que deben tener al menos un elemento, @NotNull para el resto de tipos (números envueltos, fechas, enums, objetos anidados).

Qué está evaluando la pregunta

@NotNull solo comprueba que no sea null; @NotEmpty comprueba que no sea null ni tenga longitud 0; @NotBlank comprueba además que no esté compuesta únicamente por espacios en blanco (ideal para strings).

Nivel 2 (Intermedio) ¿Qué cabecera HTTP debe incluir obligatoriamente una respuesta HTTP 201 Created según la especificación REST y cómo se genera en Spring? Ver respuesta

Respuesta razonada

La cabecera Location, con la URI del recurso recién creado, para que el cliente sepa dónde consultarlo sin construir la URL a mano. Lo habitual es devolver también la representación del recurso en el cuerpo.

En Spring se construye a partir de la URL de la petición actual: ServletUriComponentsBuilder.fromCurrentRequest().path("/{id}").buildAndExpand(id).toUri(), y se devuelve con ResponseEntity.created(uri).body(dto). Así la URI es correcta aunque la aplicación esté detrás de un proxy (siempre que se configure server.forward-headers-strategy).

Qué está evaluando la pregunta

Debe incluir la cabecera Location con la URL del nuevo recurso creado, generada mediante ServletUriComponentsBuilder.fromCurrentRequest().path("/{id}").buildAndExpand(id).toUri().

Nivel 3 (Entrevista Senior) Si devuelves una entidad JPA directamente en un endpoint GET y esa entidad tiene una relación bidireccional @OneToMany / @ManyToOne, ¿qué ocurre durante la serialización con Jackson? Ver respuesta

Respuesta razonada

Jackson serializa la tarea, que tiene un proyecto, que tiene una lista de tareas, cada una con su proyecto... y entra en recursión infinita hasta un StackOverflowError, que Spring envuelve en HttpMessageNotWritableException. Antes de llegar ahí pueden aparecer otros problemas: LazyInitializationException si la sesión de Hibernate ya está cerrada, o decenas de consultas extra (N+1) si sigue abierta con open-in-view.

Se puede parchear con @JsonManagedReference y @JsonBackReference o con @JsonIgnore, pero eso mete decisiones de presentación en la entidad. La solución correcta es no serializar entidades: mapear a un DTO que contiene exactamente los campos que la API quiere exponer, por ejemplo el id y el nombre del proyecto, no el objeto entero.

Qué está evaluando la pregunta

Jackson entrará en una recursión infinita intentando serializar el padre, que contiene al hijo, que contiene al padre... hasta provocar un StackOverflowError que tira el hilo de Tomcat. Por eso los DTOs con records son obligatorios.

Reto del tutor

Un candidato dice: "Para avisar que no encontré un usuario, lanzo throw new ResponseStatusException(HttpStatus.NOT_FOUND) dentro de mi método UsuarioService.buscar()". ¿Qué opinas de esta práctica?

Lo que una respuesta suele tener bien

Produce el código HTTP 404 deseado en la respuesta web.

Lo que suele quedarse en la superficie

Decir que "funciona bien porque Spring lo soporta".

El concepto de fondo que suele faltar

No entender que acopla la capa de negocio al protocolo web HTTP: si ese mismo servicio se usa desde un consumidor de eventos Kafka, un comando CLI o un batch, los conceptos de HttpStatus no tienen ningún sentido.

Cómo lo respondería alguien senior en una entrevista

Es una violación de la separación de capas: los servicios deben lanzar excepciones de negocio puras (UsuarioNoEncontradoException extends RuntimeException). Debe ser el @RestControllerAdvice de la capa web quien atrape la excepción de dominio y decida traducirla a un HTTP 404 ProblemDetail. Así el servicio de negocio queda 100% puro y reutilizable en cualquier contexto no web.

Proyecto «Pedidos» · paso 13 de 30

API REST de pedidos

Exponer los casos de uso por HTTP con un contrato claro y errores estándar.

De qué va el proyecto

El backend de una tienda online: clientes, catálogo de productos y pedidos con sus líneas, pagos y envíos. Empieza como un proyecto Maven con records y termina como un sistema con API REST, seguridad, mensajería, contenedores y observabilidad. Cada paso construye sobre el anterior, así que trabaja siempre en el mismo repositorio.

Qué hacer

  1. Crea POST /pedidos, GET /pedidos/{id}, POST /pedidos/{id}/confirmar y POST /pedidos/{id}/cancelar con DTOs en records.
  2. Valida la entrada con Bean Validation (al menos una línea, cantidades positivas).
  3. Traduce las excepciones de dominio a ProblemDetail en un @RestControllerAdvice (404, 409, 400).

Está listo cuando

  • Crear un pedido devuelve 201 con la cabecera Location.
  • Confirmar un pedido cancelado devuelve 409 con un ProblemDetail que explica el motivo.

Katas: ejercicios cortos

Reglas de oro

  • Nunca expongas entidades @Entity en tu API REST; usa siempre records DTO.
  • Lanza excepciones puras de dominio en servicios y usa @RestControllerAdvice para mapearlas a ProblemDetail.
  • Usa @NotBlank para strings que no deben ser vacíos ni contener solo espacios.
Se guarda automáticamente

Fuentes oficiales

Revisado el . Este día en Markdown