====== 03 - Capa de presentación ====== La **capa de presentación** adapta la comunicación entre clientes externos y las capacidades de la aplicación. En la arquitectura de referencia, sus controladores están en ''controller''. ===== 1. Responsabilidades ===== La presentación recibe solicitudes, interpreta y valida su formato, invoca la operación correspondiente y transforma el resultado o error en una respuesta HTTP. ^ Le corresponde ^ No le corresponde ^ | Interpretar la solicitud y aplicar validaciones de entrada | Decidir reglas e invariantes del negocio | | Adaptar entre DTO externos y datos de la aplicación | Acceder directamente a la base de datos | | Construir respuestas y traducir errores a HTTP | Exponer modelos internos sin revisar qué información contienen | El controlador coordina el flujo; las reglas de negocio pertenecen a la aplicación o al dominio. Así, la misma capacidad puede invocarse desde otros puntos de entrada sin duplicar reglas. ===== 2. DTO de entrada y salida ===== Los **DTO** definen los datos que cruzan la frontera HTTP. Se diseñan para el contrato externo y no tienen por qué coincidir con los modelos internos. Un ejemplo importante es el registro de usuarios: el cliente envía una contraseña, pero la respuesta no debe devolver ni la contraseña ni su hash. public record RegisterUserRequest( @NotBlank String email, @NotBlank String password, @NotBlank String displayName) {} public record UserResponse( Long id, String email, String displayName) {} El DTO de entrada puede contener datos sensibles necesarios para la operación. El de salida incluye solo los datos que el cliente necesita conocer. El mapeo entre DTO y modelos internos debe ser explícito para evitar exponer campos por accidente. ===== 3. Validación de entrada ===== La presentación valida que la solicitud tenga la forma esperada por el contrato. Las reglas que determinan si la operación está permitida siguen correspondiendo al dominio. ^ Validación de entrada ^ Regla de negocio ^ | ¿El correo tiene un formato aceptable? | ¿Ese correo ya está registrado? | | ¿La contraseña cumple la longitud mínima del contrato? | ¿El usuario puede realizar esta operación? | | ¿Falta un campo obligatorio? | ¿El estado actual permite el cambio solicitado? | **Jakarta Bean Validation** define anotaciones como ''@NotBlank'', ''@Email'' y ''@Size'' para declarar restricciones sobre los datos. **Spring** se integra con esta API y activa la validación al recibir el DTO marcado con ''@Valid''; normalmente **Hibernate Validator** actúa como proveedor: public record RegisterUserRequest( @NotBlank @Email String email, @NotBlank @Size(min = 12) String password, @NotBlank String displayName) {} @PostMapping("/users") public ResponseEntity register( @Valid @RequestBody RegisterUserRequest request) { // Delegar el registro y construir la respuesta. } La validación del formato evita procesar peticiones mal formadas. No sustituye las comprobaciones de negocio: por ejemplo, comprobar si el correo ya existe depende del estado de la aplicación. ===== 4. Respuestas y excepciones ===== La capa transforma resultados de aplicación en respuestas HTTP. En **Spring Boot**, ''ResponseEntity'' permite controlar el **estado**, las **cabeceras** y el **cuerpo** de la respuesta. return ResponseEntity .status(HttpStatus.CREATED) .body(userResponse); Si basta la respuesta estándar, no es necesario usar ''ResponseEntity'' en todos los endpoints. Se utiliza cuando interesa expresar explícitamente el estado, las cabeceras u otras opciones de la respuesta. Los errores de aplicación deben convertirse en respuestas HTTP consistentes, sin revelar detalles internos. En **Spring Boot**, ''@RestControllerAdvice'' centraliza esta traducción y ''@ExceptionHandler'' asocia una excepción con el método que construye su respuesta: public record ApiError(String code, String message) {} @RestControllerAdvice public class ApiExceptionHandler { @ExceptionHandler(UserAlreadyExistsException.class) ResponseEntity handleUserAlreadyExists( UserAlreadyExistsException exception) { return ResponseEntity.status(HttpStatus.CONFLICT) .body(new ApiError( "USER_ALREADY_EXISTS", "No se puede crear el usuario")); } } ''ApiError'' es un **DTO** de ejemplo, no una clase obligatoria de Spring. La aplicación puede definir su propio formato de error o utilizar otro formato acordado en el contrato público. El flujo es: el dominio o la aplicación lanza una excepción propia; el manejador la reconoce y la traduce a un estado HTTP y un cuerpo de error público. El controlador no necesita capturarla en cada operación. Si varias excepciones significan lo mismo para el cliente, se pueden tratar en un único manejador y devolver el mismo estado y respuesta: @ExceptionHandler({ EmailAlreadyRegisteredException.class, UsernameAlreadyTakenException.class }) ResponseEntity handleUserConflict(Exception exception) { return ResponseEntity.status(HttpStatus.CONFLICT) .body(new ApiError( "USER_ALREADY_EXISTS", "No se puede crear el usuario")); } En una **API pública**, hay que contemplar también excepciones inesperadas. En el mismo consejo se puede añadir un manejador genérico que devuelva una respuesta estándar con estado **500**, sin exponer mensajes internos, trazas ni datos sensibles. El detalle completo se registra en el servidor para diagnosticar el fallo: @ExceptionHandler(Exception.class) ResponseEntity handleUnexpected(Exception exception) { log.error("Unexpected API error", exception); return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) .body(new ApiError( "INTERNAL_ERROR", "Ha ocurrido un error inesperado")); } En una API pública, toda excepción no prevista debe producir una respuesta de error estándar y segura. No devuelvas al cliente mensajes internos, trazas ni información que facilite conocer la implementación. ===== 5. Pruebas de presentación ===== Las pruebas comprueban que la interfaz HTTP respeta su contrato: * Ruta y método correctos; entrada JSON convertida al DTO esperado. * Validaciones de entrada y traducción de excepciones conocidas y no previstas. * Estado y cuerpo de respuesta, incluyendo que no se devuelven datos sensibles. * Respuestas genéricas sin mensajes internos ni trazas ante fallos inesperados. * Delegación a la operación de aplicación con los datos adecuados. Con **MockMvc** se integra la capa web real —rutas, conversión, validación y serialización— y se simulan los servicios o casos de uso. No se prueba aquí la lógica de negocio ni la persistencia; esas responsabilidades se verifican en sus propias pruebas. @WebMvcTest(UserController.class) class UserControllerTest { @Autowired MockMvc mockMvc; @MockitoBean RegisterUser registerUser; @Test void responseDoesNotExposePassword() throws Exception { when(registerUser.execute(any())) .thenReturn(new UserResult(7L, "ana@example.test", "Ana")); mockMvc.perform(post("/users") .contentType(MediaType.APPLICATION_JSON) .content(""" {"email":"ana@example.test", "password":"long-password-value", "displayName":"Ana"} """)) .andExpect(status().isCreated()) .andExpect(jsonPath("$.email").value("ana@example.test")) .andExpect(jsonPath("$.password").doesNotExist()); } } La prueba integra la frontera web y aísla la operación de aplicación. No comprueba el registro real ni el almacenamiento de la contraseña; comprueba que la respuesta HTTP respeta el contrato y no expone el campo sensible.