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.

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.

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.

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<UserResponse> 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.

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<ApiError> 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<ApiError> 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<ApiError> 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.

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.

  • clase/daw/dws/1eval/presentacion.txt
  • Última modificación: 2026/10/03 11:05
  • por cesguiro