Spring Boot

SpringBoot

Tutorial SpringBoot: Controlador REST reactivo funcional

Aprende a programar un API REST reactiva en Spring WebFlux sobre Spring Boot desarrollando controladores REST reactivos funcionales con rutas y métodos manejadores.

Aprende SpringBoot GRATIS y certifícate

Explicación de clases para controladores funcionales

En Spring WebFlux, los controladores funcionales utilizan un enfoque basado en funciones para manejar las solicitudes HTTP. Este enfoque se basa en varias clases clave que facilitan la creación y gestión de rutas y manejadores de solicitudes.

Una de las clases principales es RouterFunction, que representa una función que enruta una solicitud a un manejador. Se utiliza junto con la clase RouterFunctions para definir las rutas de la aplicación. Por ejemplo, podemos usar RouterFunctions.route() para establecer una ruta específica y asociarla con un manejador.

Otra clase esencial es HandlerFunction, que es una interfaz funcional que maneja una solicitud y produce una respuesta. Los manejadores se implementan típicamente como funciones lambda o métodos que reciben un ServerRequest y devuelven un Mono<ServerResponse>.

El objeto ServerRequest encapsula la información de la solicitud HTTP entrante, proporcionando métodos para acceder a parámetros, encabezados y el cuerpo de la solicitud. Por otro lado, ServerResponse se utiliza para construir la respuesta HTTP que se enviará al cliente, permitiendo establecer el código de estado, los encabezados y el cuerpo de la respuesta.

Además, RequestPredicate es una interfaz funcional que sirve para definir condiciones bajo las cuales se enruta una solicitud a un manejador. Estas condiciones pueden basarse en el método HTTP, la ruta, los encabezados u otros atributos de la solicitud.

Para configurar los controladores funcionales en la aplicación, es común definir una clase de configuración que contiene los métodos para construir las rutas y los manejadores. Esta clase se anota con @Configuration para que Spring la detecte y procese adecuadamente.

Por ejemplo, podemos tener una configuración como la siguiente:

@Configuration
public class RouterConfig {

    @Bean
    public RouterFunction<ServerResponse> route(Handler handler) {
        return RouterFunctions
                .route(RequestPredicates.GET("/api/funcional/ejemplo"), handler::manejarEjemplo);
    }
}

En este ejemplo, estamos definiendo una ruta que responde a solicitudes GET en el path /api/funcional/ejemplo, y la estamos asociando con el método manejarEjemplo del objeto handler.

El handler es una clase que implementa los métodos necesarios para manejar las solicitudes. Un ejemplo de un manejador podría ser:

@Component
public class Handler {

    public Mono<ServerResponse> manejarEjemplo(ServerRequest request) {
        // Lógica del manejador
        return ServerResponse.ok().body(BodyInserters.fromValue("Respuesta de ejemplo"));
    }
}

Aquí, el método manejarEjemplo recibe un ServerRequest y devuelve un Mono<ServerResponse>, siguiendo el modelo reactivo. Utiliza ServerResponse.ok() para construir una respuesta exitosa y BodyInserters.fromValue() para insertar el cuerpo de la respuesta.

Es importante destacar que, al utilizar este enfoque funcional, se aprovechan las capacidades de la programación funcional y reactiva de Java, permitiendo un código más conciso y expresivo.

Por último, para manejar excepciones y errores, se puede utilizar la clase ErrorWebExceptionHandler, que permite definir cómo se gestionan las excepciones no controladas en la aplicación.

Métodos GET usando handlers funcionales en servicio

Al implementar métodos GET utilizando handlers funcionales en un servicio con Spring WebFlux, se adopta un enfoque más declarativo y funcional para manejar las solicitudes HTTP. Este estilo permite definir rutas y manejadores de forma concisa y expresiva.

Para comenzar, es esencial definir las rutas que atenderán las solicitudes GET. Utilizamos la función route() de RouterFunctions para asociar una ruta específica con un handler. Por ejemplo:

@Bean
public RouterFunction<ServerResponse> routes(Handler handler) {
    return RouterFunctions.route()
        .GET("/api/funcional/usuarios", handler::obtenerUsuarios)
        .GET("/api/funcional/usuarios/{id}", handler::obtenerUsuarioPorId)
        .build();
}

En este ejemplo, estamos configurando dos rutas GET:

  1. /api/funcional/usuarios: para obtener la lista de usuarios.
  2. /api/funcional/usuarios/{id}: para obtener un usuario específico por su identificador.

El siguiente paso es implementar los handlers correspondientes en el servicio. Estos handlers son métodos que reciben un ServerRequest y devuelven un Mono<ServerResponse>. Veamos cómo se podrían definir:

@Component
public class Handler {

    private final UsuarioService usuarioService;

    public Handler(UsuarioService usuarioService) {
        this.usuarioService = usuarioService;
    }

    public Mono<ServerResponse> obtenerUsuarios(ServerRequest request) {
        Flux<Usuario> usuarios = usuarioService.listarUsuarios();
        return ServerResponse.ok()
            .contentType(MediaType.APPLICATION_JSON)
            .body(usuarios, Usuario.class);
    }

    public Mono<ServerResponse> obtenerUsuarioPorId(ServerRequest request) {
        String id = request.pathVariable("id");
        Mono<Usuario> usuario = usuarioService.buscarUsuarioPorId(id);

        return usuario.flatMap(u -> ServerResponse.ok()
                .contentType(MediaType.APPLICATION_JSON)
                .bodyValue(u))
            .switchIfEmpty(ServerResponse.notFound().build());
    }
}

En el handler obtenerUsuarios, se obtiene un Flux<Usuario> llamando al servicio listarUsuarios(). Luego, se construye una respuesta con ServerResponse.ok(), estableciendo el tipo de contenido como JSON y pasando el flujo de usuarios en el cuerpo de la respuesta.

Para el handler obtenerUsuarioPorId, extraemos el parámetro {id} de la ruta utilizando request.pathVariable("id"). Se busca el usuario con buscarUsuarioPorId(id), que devuelve un Mono<Usuario>. Utilizamos flatMap para manejar el caso en que el usuario exista y switchIfEmpty para responder con un 404 Not Found si no se encuentra el usuario.

Es importante destacar el uso de métodos como body() y bodyValue() para construir el cuerpo de la respuesta. Mientras que body() se utiliza con un Publisher (como Flux o Mono), bodyValue() se usa cuando ya tenemos el objeto a devolver.

Además, para manejar parámetros de consulta en solicitudes GET, podemos utilizar request.queryParam("nombre"):

public Mono<ServerResponse> buscarUsuariosPorNombre(ServerRequest request) {
    String nombre = request.queryParam("nombre").orElse("");
    Flux<Usuario> usuarios = usuarioService.buscarUsuariosPorNombre(nombre);

    return ServerResponse.ok()
        .contentType(MediaType.APPLICATION_JSON)
        .body(usuarios, Usuario.class);
}

En este caso, si el parámetro nombre no está presente, se utilizará una cadena vacía. El servicio buscarUsuariosPorNombre(nombre) devuelve un Flux<Usuario> con los usuarios cuyo nombre coincide con el criterio.

Al utilizar handlers funcionales, también es posible manejar encabezados y otros atributos de la solicitud. Por ejemplo, si necesitamos acceder a un encabezado personalizado:

public Mono<ServerResponse> obtenerUsuarioDesdeEncabezado(ServerRequest request) {
    String token = request.headers().firstHeader("Authorization");
    Mono<Usuario> usuario = usuarioService.buscarUsuarioPorToken(token);

    return usuario.flatMap(u -> ServerResponse.ok()
            .contentType(MediaType.APPLICATION_JSON)
            .bodyValue(u))
        .switchIfEmpty(ServerResponse.status(HttpStatus.UNAUTHORIZED).build());
}

Aquí, extraemos el encabezado Authorization y buscamos el usuario asociado al token. Si el usuario existe, se devuelve con un 200 OK; de lo contrario, se responde con un 401 Unauthorized.

Cuando se trabaja con datos reactivos, es esencial gestionar adecuadamente el flujo y posibles errores. El uso de operadores como flatMap, switchIfEmpty y onErrorResume permite manejar diferentes escenarios de forma eficiente y reactiva.

Además, podemos aprovechar las expresiones lambda de Java para hacer el código más conciso:

public Mono<ServerResponse> listarUsuarios(ServerRequest request) {
    return ServerResponse.ok()
        .contentType(MediaType.APPLICATION_JSON)
        .body(usuarioService.listarUsuarios(), Usuario.class);
}

En este ejemplo simplificado, directamente devolvemos la respuesta sin necesidad de asignar el flujo de usuarios a una variable intermedia.

Finalmente, es crucial asegurarse de que los handlers sean sin estado y thread-safe, dado que en aplicaciones reactivas pueden ser llamados concurrentemente. Mantener la lógica de negocio en servicios separados ayuda a mantener una arquitectura limpia y modular.

Métodos POST, PUT y PATCH usando handlers funcionales en servicio

Al implementar métodos POST, PUT y PATCH en Spring WebFlux utilizando handlers funcionales, se adopta un enfoque declarativo y moderno que aprovecha las ventajas de la programación funcional en Java. Estos métodos se utilizan para crear y actualizar recursos respectivamente, y su correcta implementación es esencial para construir APIs RESTful eficientes.

Para comenzar, es necesario definir las rutas que gestionarán las solicitudes HTTP correspondientes. Utilizamos la función route() de RouterFunctions para asociar cada ruta con su handler adecuado. Por ejemplo:

@Bean
public RouterFunction<ServerResponse> routes(Handler handler) {
    return RouterFunctions.route()
        .POST("/api/funcional/usuarios", handler::crearUsuario)
        .PUT("/api/funcional/usuarios/{id}", handler::actualizarUsuario)
        .PATCH("/api/funcional/usuarios/{id}", handler::modificarUsuarioParcialmente)
        .build();
}

En este fragmento de código, establecemos tres rutas:

  1. POST /api/funcional/usuarios: para la creación de un nuevo usuario.
  2. PUT /api/funcional/usuarios/{id}: para la actualización completa de un usuario existente.
  3. PATCH /api/funcional/usuarios/{id}: para la modificación parcial de un usuario.

Una vez definidas las rutas, es fundamental implementar los handlers correspondientes. Estos handlers gestionan el cuerpo de la solicitud, realizan las operaciones necesarias y construyen la respuesta adecuada.

Handler para método POST

El handler para el método POST permite crear un nuevo usuario a partir de los datos proporcionados en el cuerpo de la solicitud:

public Mono<ServerResponse> crearUsuario(ServerRequest request) {
    Mono<Usuario> usuarioMono = request.bodyToMono(Usuario.class);

    return usuarioMono.flatMap(usuario -> usuarioService.crearUsuario(usuario)
            .flatMap(usuarioCreado -> ServerResponse.status(HttpStatus.CREATED)
                .contentType(MediaType.APPLICATION_JSON)
                .bodyValue(usuarioCreado)))
        .onErrorResume(error -> ServerResponse.badRequest()
            .bodyValue("Error al crear el usuario: " + error.getMessage()));
}

En este handler:

  • Utilizamos bodyToMono(Usuario.class) para convertir el cuerpo de la solicitud en un objeto Usuario.
  • Aplicamos flatMap para manipular el Mono<Usuario> resultante y llamar al servicio crearUsuario(usuario).
  • Si la creación es exitosa, construimos una respuesta con código de estado 201 Created, establecemos el tipo de contenido y enviamos el usuario creado en el cuerpo de la respuesta.
  • En caso de error, gestionamos la excepción con onErrorResume, devolviendo una respuesta 400 Bad Request con un mensaje descriptivo.

Handler para método PUT

El handler para el método PUT gestiona la actualización completa de un usuario existente:

public Mono<ServerResponse> actualizarUsuario(ServerRequest request) {
    String id = request.pathVariable("id");
    Mono<Usuario> usuarioMono = request.bodyToMono(Usuario.class);

    return usuarioService.buscarUsuarioPorId(id)
        .zipWith(usuarioMono, (usuarioExistente, usuarioActualizado) -> {
            usuarioActualizado.setId(usuarioExistente.getId());
            return usuarioActualizado;
        })
        .flatMap(usuario -> usuarioService.actualizarUsuario(usuario))
        .flatMap(usuarioActualizado -> ServerResponse.ok()
            .contentType(MediaType.APPLICATION_JSON)
            .bodyValue(usuarioActualizado))
        .switchIfEmpty(ServerResponse.notFound().build())
        .onErrorResume(error -> ServerResponse.badRequest()
            .bodyValue("Error al actualizar el usuario: " + error.getMessage()));
}

En este handler:

  • Extraemos el id del usuario desde el path variable.
  • Convertimos el cuerpo de la solicitud en un Mono<Usuario>.
  • Buscamos el usuario existente con buscarUsuarioPorId(id).
  • Utilizamos zipWith para combinar el usuario existente con el actualizado, asegurando que el id se mantenga coherente.
  • Actualizamos el usuario llamando a actualizarUsuario(usuario) del servicio.
  • Si la operación es exitosa, devolvemos una respuesta 200 OK con el usuario actualizado.
  • Si el usuario no existe, respondemos con un 404 Not Found.
  • Gestionamos posibles errores devolviendo una respuesta 400 Bad Request.

Handler para método PATCH

El handler para el método PATCH permite modificar parcialmente los datos de un usuario:

public Mono<ServerResponse> modificarUsuarioParcialmente(ServerRequest request) {
    String id = request.pathVariable("id");
    Mono<Map<String, Object>> updatesMono = request.bodyToMono(new ParameterizedTypeReference<Map<String, Object>>() {});

    return usuarioService.buscarUsuarioPorId(id)
        .zipWith(updatesMono, (usuarioExistente, updates) -> {
            updates.forEach((key, value) -> {
                switch (key) {
                    case "nombre":
                        usuarioExistente.setNombre((String) value);
                        break;
                    case "email":
                        usuarioExistente.setEmail((String) value);
                        break;
                    // Agregar más campos según sea necesario
                }
            });
            return usuarioExistente;
        })
        .flatMap(usuario -> usuarioService.actualizarUsuario(usuario))
        .flatMap(usuarioActualizado -> ServerResponse.ok()
            .contentType(MediaType.APPLICATION_JSON)
            .bodyValue(usuarioActualizado))
        .switchIfEmpty(ServerResponse.notFound().build())
        .onErrorResume(error -> ServerResponse.badRequest()
            .bodyValue("Error al modificar el usuario: " + error.getMessage()));
}

En este handler:

  • Obtenemos el id del usuario desde el path variable.
  • Convertimos el cuerpo de la solicitud en un Mono<Map<String, Object>>, lo que nos permite manejar actualizaciones parciales.
  • Buscamos el usuario existente y utilizamos zipWith para combinarlo con el mapa de actualizaciones.
  • Iteramos sobre las entradas del mapa y actualizamos los campos correspondientes del usuario.
  • Llamamos al servicio actualizarUsuario(usuario) para persistir los cambios.
  • Devolvemos una respuesta 200 OK con el usuario modificado o un 404 Not Found si el usuario no existe.
  • Manejamos errores y excepciones devolviendo una respuesta 400 Bad Request.

Consideraciones adicionales

Es importante destacar algunos aspectos clave al trabajar con handlers funcionales para métodos POST, PUT y PATCH:

Validación de datos: Antes de procesar la solicitud, es recomendable validar los datos recibidos. Esto puede realizarse mediante librerías de validación o manualmente, verificando que los campos obligatorios estén presentes y cumplan con las restricciones necesarias.

Gestión de errores: Utilizar operadores como onErrorResume permite manejar excepciones y errores, proporcionando respuestas significativas al cliente y evitando que la aplicación falle silenciosamente.

Inmutabilidad: Al modificar objetos, especialmente en un contexto reactivo, es preferible trabajar con objetos inmutables o crear nuevas instancias para evitar efectos secundarios indeseados.

Uso de Mono y Flux: Los handlers trabajan con Mono cuando se espera un único resultado y con Flux para múltiples resultados. En el caso de POST, PUT y PATCH, generalmente se utiliza Mono<Usuario>.

Servicios asociados

Para que los handlers funcionen correctamente, es necesario contar con un servicio que gestione la lógica de negocio. Un ejemplo simplificado del servicio podría ser:

@Service
public class UsuarioService {

    private final UsuarioRepository usuarioRepository;

    public UsuarioService(UsuarioRepository usuarioRepository) {
        this.usuarioRepository = usuarioRepository;
    }

    public Mono<Usuario> crearUsuario(Usuario usuario) {
        return usuarioRepository.save(usuario);
    }

    public Mono<Usuario> buscarUsuarioPorId(String id) {
        return usuarioRepository.findById(id);
    }

    public Mono<Usuario> actualizarUsuario(Usuario usuario) {
        return usuarioRepository.save(usuario);
    }
}

En este servicio:

  • crearUsuario(usuario) guarda un nuevo usuario en el repositorio.
  • buscarUsuarioPorId(id) busca un usuario existente por su id.
  • actualizarUsuario(usuario) persiste los cambios de un usuario existente.

La interacción con el repositorio reactivo permite mantener el flujo reactivo de datos desde la capa de datos hasta la respuesta al cliente.

Respuestas adecuadas

Al construir las respuestas, es esencial establecer el código de estado HTTP correcto:

  • 201 Created para indicar que un recurso ha sido creado exitosamente.
  • 200 OK para confirmar que una operación ha sido exitosa.
  • 400 Bad Request cuando hay errores en la solicitud del cliente.
  • 404 Not Found si el recurso solicitado no existe.

Además, es importante establecer el Content-Type apropiado, generalmente MediaType.APPLICATION_JSON, para que el cliente interprete correctamente la respuesta.

Ejemplo completo de configuración

Integrando todo lo anterior, la configuración completa podría quedar de la siguiente manera:

@Configuration
public class RouterConfig {

    @Bean
    public RouterFunction<ServerResponse> routes(Handler handler) {
        return RouterFunctions.route()
            .POST("/api/funcional/usuarios", handler::crearUsuario)
            .PUT("/api/funcional/usuarios/{id}", handler::actualizarUsuario)
            .PATCH("/api/funcional/usuarios/{id}", handler::modificarUsuarioParcialmente)
            .build();
    }
}

Y los handlers implementados:

@Component
public class Handler {

    private final UsuarioService usuarioService;

    public Handler(UsuarioService usuarioService) {
        this.usuarioService = usuarioService;
    }

    // Handlers implementados anteriormente...
}

La implementación de métodos POST, PUT y PATCH utilizando handlers funcionales en Spring WebFlux ofrece una forma clara y concisa de gestionar las operaciones de creación y actualización de recursos. Al aprovechar las características de la programación funcional y reactiva, se logra un código más fluido y eficiente, facilitando el desarrollo de servicios web modernos y escalables.

Métodos DELETE usando handlers funcionales en servicio

Al implementar métodos DELETE utilizando handlers funcionales en Spring WebFlux, se sigue un enfoque funcional que permite manejar solicitudes HTTP de forma concisa y expresiva. Este método se utiliza para eliminar recursos identificados por un parámetro, generalmente un identificador único.

Primero, es necesario definir las rutas que atenderán las solicitudes DELETE. Utilizamos el método DELETE de RouterFunctions.route() para asociar una ruta específica con su handler correspondiente:

@Bean
public RouterFunction<ServerResponse> routes(Handler handler) {
    return RouterFunctions.route()
        .DELETE("/api/funcional/usuarios/{id}", handler::eliminarUsuario)
        .build();
}

En este ejemplo, se configura una ruta DELETE en el path /api/funcional/usuarios/{id}, donde {id} es un path variable que representa el identificador del usuario a eliminar. La solicitud será manejada por el método eliminarUsuario del handler.

A continuación, se implementa el handler que gestionará la lógica de eliminación:

@Component
public class Handler {

    private final UsuarioService usuarioService;

    public Handler(UsuarioService usuarioService) {
        this.usuarioService = usuarioService;
    }

    public Mono<ServerResponse> eliminarUsuario(ServerRequest request) {
        String id = request.pathVariable("id");

        return usuarioService.buscarUsuarioPorId(id)
            .flatMap(usuario -> usuarioService.eliminarUsuario(usuario.getId())
                .then(ServerResponse.noContent().build()))
            .switchIfEmpty(ServerResponse.notFound().build())
            .onErrorResume(error -> ServerResponse.status(HttpStatus.INTERNAL_SERVER_ERROR)
                .bodyValue("Error al eliminar el usuario: " + error.getMessage()));
    }
}

En este handler:

  • Se extrae el id del usuario desde el path variable de la solicitud.
  • Se busca el usuario existente utilizando usuarioService.buscarUsuarioPorId(id), que devuelve un Mono<Usuario>.
  • Si el usuario existe, se procede a eliminarlo llamando a usuarioService.eliminarUsuario(usuario.getId()).
  • La operación de eliminación devuelve un Mono<Void>. Al utilizar then(ServerResponse.noContent().build()), se espera a que la eliminación se complete y luego se devuelve una respuesta con código de estado 204 No Content.
  • Si el usuario no se encuentra, se responde con un 404 Not Found utilizando switchIfEmpty(ServerResponse.notFound().build()).
  • Se gestionan los posibles errores con onErrorResume, devolviendo una respuesta con código de estado 500 Internal Server Error y un mensaje descriptivo.

El servicio UsuarioService debe implementar el método eliminarUsuario que realice la operación de eliminación en el repositorio:

@Service
public class UsuarioService {

    private final UsuarioRepository usuarioRepository;

    public UsuarioService(UsuarioRepository usuarioRepository) {
        this.usuarioRepository = usuarioRepository;
    }

    public Mono<Usuario> buscarUsuarioPorId(String id) {
        return usuarioRepository.findById(id);
    }

    public Mono<Void> eliminarUsuario(String id) {
        return usuarioRepository.deleteById(id);
    }
}

En este servicio:

  • buscarUsuarioPorId(id) busca el usuario por su identificador y devuelve un Mono<Usuario>.
  • eliminarUsuario(id) elimina el usuario por su id y devuelve un Mono<Void> que indica la finalización de la operación.

Es importante manejar adecuadamente la reactividad en estas operaciones. Al utilizar métodos como flatMap, se asegura que las operaciones sean no bloqueantes y se encadenen de forma correcta en el flujo reactivo.

Gestión de errores y validaciones

Al eliminar recursos, es fundamental verificar que el recurso existe antes de intentar eliminarlo. Además, es aconsejable implementar validaciones adicionales según las necesidades de la aplicación.

Por ejemplo, si se desea verificar que el usuario a eliminar no tiene dependencias que impidan su eliminación, se podría extender el handler de la siguiente manera:

public Mono<ServerResponse> eliminarUsuario(ServerRequest request) {
    String id = request.pathVariable("id");

    return usuarioService.buscarUsuarioPorId(id)
        .flatMap(usuario -> comprobarDependenciasUsuario(usuario)
            .flatMap(sinDependencias -> {
                if (sinDependencias) {
                    return usuarioService.eliminarUsuario(usuario.getId())
                        .then(ServerResponse.noContent().build());
                } else {
                    return ServerResponse.status(HttpStatus.CONFLICT)
                        .bodyValue("El usuario tiene dependencias y no puede ser eliminado");
                }
            }))
        .switchIfEmpty(ServerResponse.notFound().build())
        .onErrorResume(error -> ServerResponse.status(HttpStatus.INTERNAL_SERVER_ERROR)
            .bodyValue("Error al eliminar el usuario: " + error.getMessage()));
}

private Mono<Boolean> comprobarDependenciasUsuario(Usuario usuario) {
    // Lógica para comprobar dependencias
    return Mono.just(true); // Supongamos que no tiene dependencias
}

En este caso:

  • Se añade una comprobación de dependencias mediante el método comprobarDependenciasUsuario(usuario), que devuelve un Mono<Boolean>.
  • Si el usuario no tiene dependencias, se procede a eliminarlo y se devuelve un 204 No Content.
  • Si existen dependencias, se responde con un 409 Conflict y un mensaje descriptivo.
  • Se mantiene la gestión de errores y el manejo del caso en que el usuario no existe.

Uso de filtros y middlewares

Es posible incorporar filtros o middlewares en las rutas para añadir funcionalidades transversales, como autenticación o logging. Por ejemplo, para proteger la ruta de eliminación y requerir autenticación:

@Bean
public RouterFunction<ServerResponse> routes(Handler handler) {
    return RouterFunctions.route()
        .DELETE("/api/funcional/usuarios/{id}", handler::eliminarUsuario)
        .filter(autenticacionFilter())
        .build();
}

private HandlerFilterFunction<ServerResponse, ServerResponse> autenticacionFilter() {
    return (request, next) -> {
        // Lógica de autenticación
        String token = request.headers().firstHeader("Authorization");
        if (esTokenValido(token)) {
            return next.handle(request);
        } else {
            return ServerResponse.status(HttpStatus.UNAUTHORIZED).build();
        }
    };
}

private boolean esTokenValido(String token) {
    // Verificación del token
    return token != null && token.equals("tokenValido");
}

Aquí:

  • Se utiliza filter(autenticacionFilter()) para aplicar un filtro de autenticación a las rutas.
  • El filtro comprueba la validez del token de autorización y, si es válido, permite continuar con la solicitud.
  • Si el token no es válido, se devuelve una respuesta 401 Unauthorized.

Buenas prácticas

Al implementar métodos DELETE utilizando handlers funcionales, es recomendable seguir ciertas buenas prácticas:

Idempotencia: Aunque las solicitudes DELETE deben ser idempotentes según la especificación HTTP, es importante asegurarse de que múltiples solicitudes para eliminar el mismo recurso no generen errores inesperados.

Seguridad: Proteger las rutas de eliminación con autenticación y autorizaciones adecuadas para evitar eliminaciones no autorizadas.

Respuestas adecuadas: Utilizar los códigos de estado HTTP correctos para reflejar el resultado de la operación. Por ejemplo, 204 No Content cuando la eliminación es exitosa y no se devuelve contenido en el cuerpo de la respuesta.

Gestión de errores: Proporcionar mensajes de error claros y manejar excepciones de forma que no se revele información sensible sobre el sistema.

Registro y monitorización: Implementar mecanismos de logging para registrar las operaciones de eliminación y facilitar la monitorización y auditoría de las acciones realizadas.

Extensión a múltiples eliminaciones

En casos donde se requiera eliminar múltiples recursos en una sola solicitud, se puede implementar una ruta que acepte un cuerpo con la lista de identificadores:

@Bean
public RouterFunction<ServerResponse> routes(Handler handler) {
    return RouterFunctions.route()
        .DELETE("/api/funcional/usuarios", handler::eliminarUsuarios)
        .build();
}

El handler correspondiente manejaría la lista de identificadores:

public Mono<ServerResponse> eliminarUsuarios(ServerRequest request) {
    Mono<List<String>> idsMono = request.bodyToMono(new ParameterizedTypeReference<List<String>>() {});
    
    return idsMono.flatMapMany(Flux::fromIterable)
        .flatMap(id -> usuarioService.eliminarUsuario(id))
        .then(ServerResponse.noContent().build())
        .onErrorResume(error -> ServerResponse.status(HttpStatus.INTERNAL_SERVER_ERROR)
            .bodyValue("Error al eliminar usuarios: " + error.getMessage()));
}

En este ejemplo:

  • Se convierte el cuerpo de la solicitud en un Mono<List<String>> que contiene los identificadores.
  • Se utiliza flatMapMany para convertir el Mono en un Flux<String> y procesar cada id individualmente.
  • Se llama a usuarioService.eliminarUsuario(id) para eliminar cada usuario.
  • Tras completar todas las eliminaciones, se devuelve una respuesta 204 No Content.

Consideraciones sobre la concurrencia

En entornos reactivos, es crucial tener en cuenta la concurrencia y la consistencia de los datos. Al eliminar recursos, especialmente en operaciones simultáneas, pueden surgir condiciones de carrera. Es aconsejable implementar mecanismos de control como bloqueos optimistas o pesimistas si la lógica de negocio lo requiere.

Además, es importante que las operaciones en la capa de datos sean atómicas y transaccionales cuando sea necesario, para mantener la integridad de los datos.

Integración con pruebas unitarias

Para asegurar el correcto funcionamiento de los handlers DELETE, se deben escribir pruebas unitarias que validen los diferentes escenarios:

  • Eliminación exitosa de un usuario existente.
  • Intento de eliminar un usuario que no existe.
  • Gestión de errores en caso de fallos en la capa de datos.
  • Verificación de que las respuestas HTTP tienen los códigos de estado correctos.

Las pruebas se pueden implementar utilizando WebTestClient para simular solicitudes HTTP y verificar las respuestas:

@SpringBootTest
@AutoConfigureWebTestClient
public class HandlerTests {

    @Autowired
    private WebTestClient webTestClient;

    @Test
    public void testEliminarUsuarioExistente() {
        webTestClient.delete()
            .uri("/api/funcional/usuarios/{id}", "123")
            .exchange()
            .expectStatus().isNoContent();
    }

    @Test
    public void testEliminarUsuarioNoExistente() {
        webTestClient.delete()
            .uri("/api/funcional/usuarios/{id}", "999")
            .exchange()
            .expectStatus().isNotFound();
    }
}

Con estas pruebas, se valida que el handler responde adecuadamente en diferentes situaciones, asegurando la calidad y fiabilidad del servicio.

La implementación de métodos DELETE usando handlers funcionales en servicio con Spring WebFlux permite aprovechar las ventajas de la programación reactiva y funcional, ofreciendo una manera eficiente y elegante de gestionar operaciones de eliminación de recursos. Al seguir las prácticas recomendadas y prestar atención a los detalles, se pueden construir servicios robustos y escalables que cumplan con los estándares modernos de desarrollo.

Aprende SpringBoot GRATIS online

Ejercicios de esta lección Controlador REST reactivo funcional

Evalúa tus conocimientos de esta lección Controlador REST reactivo funcional con nuestros retos de programación de tipo Test, Puzzle, Código y Proyecto con VSCode, guiados por IA.

API Query By Example (QBE)

Spring Boot
Test

Identificadores y relaciones JPA

Spring Boot
Puzzle

Borrar datos de base de datos

Spring Boot
Test

Web y Test Starters

Spring Boot
Puzzle

Métodos find en repositorios

Spring Boot
Test

Controladores Spring MVC

Spring Boot
Código

Inserción de datos

Spring Boot
Test

CRUD Customers Spring MVC + Spring Data JPA

Spring Boot
Proyecto

Backend API REST con Spring Boot

Spring Boot
Proyecto

Controladores Spring REST

Spring Boot
Código

Uso de Spring con Thymeleaf

Spring Boot
Puzzle

API Specification

Spring Boot
Puzzle

Registro de usuarios

Spring Boot
Test

Crear entidades JPA

Spring Boot
Código

Asociaciones en JPA

Spring Boot
Test

Asociaciones de entidades JPA

Spring Boot
Código

Integración con Vue

Spring Boot
Test

Consultas JPQL

Spring Boot
Código

Open API y cómo agregarlo en Spring Boot

Spring Boot
Puzzle

Uso de Controladores REST

Spring Boot
Puzzle

Repositorios reactivos

Spring Boot
Test

Inyección de dependencias

Spring Boot
Test

Introducción a Spring Boot

Spring Boot
Test

CRUD y JPA Repository

Spring Boot
Puzzle

Inyección de dependencias

Spring Boot
Código

Vista en Spring MVC con Thymeleaf

Spring Boot
Test

Servicios en Spring

Spring Boot
Código

Operadores Reactivos

Spring Boot
Puzzle

Configuración de Vue

Spring Boot
Puzzle

Entidades JPA

Spring Boot
Test

Integración con Angular

Spring Boot
Test

API Specification

Spring Boot
Test

API Query By Example (QBE)

Spring Boot
Puzzle

Controladores MVC

Spring Boot
Test

Anotaciones y mapeo en JPA

Spring Boot
Puzzle

Consultas JPQL con @Query en Spring Data JPA

Spring Boot
Test

Repositorios Spring Data

Spring Boot
Test

Inyección de dependencias

Spring Boot
Puzzle

Data JPA y Mail Starters

Spring Boot
Test

Configuración de Angular

Spring Boot
Puzzle

Controladores Spring REST

Spring Boot
Test

Configuración de Controladores MVC

Spring Boot
Puzzle

Consultas JPQL con @Query en Spring Data JPA

Spring Boot
Puzzle

Actualizar datos de base de datos

Spring Boot
Test

Verificar token JWT en peticiones

Spring Boot
Test

Login de usuarios

Spring Boot
Test

Integración con React

Spring Boot
Test

Configuración de React

Spring Boot
Puzzle

Todas las lecciones de SpringBoot

Accede a todas las lecciones de SpringBoot y aprende con ejemplos prácticos de código y ejercicios de programación con IDE web sin instalar nada.

Introducción A Spring Boot

Spring Boot

Introducción Y Entorno

Spring Boot Starters

Spring Boot

Introducción Y Entorno

Inyección De Dependencias

Spring Boot

Introducción Y Entorno

Controladores Spring Mvc

Spring Boot

Spring Web

Vista En Spring Mvc Con Thymeleaf

Spring Boot

Spring Web

Controladores Spring Rest

Spring Boot

Spring Web

Open Api Y Cómo Agregarlo En Spring Boot

Spring Boot

Spring Web

Servicios En Spring

Spring Boot

Spring Web

Clientes Resttemplate Y Restclient

Spring Boot

Spring Web

Rxjava En Spring Web

Spring Boot

Spring Web

Crear Entidades Jpa

Spring Boot

Persistencia Spring Data

Asociaciones De Entidades Jpa

Spring Boot

Persistencia Spring Data

Repositorios Spring Data

Spring Boot

Persistencia Spring Data

Métodos Find En Repositorios

Spring Boot

Persistencia Spring Data

Inserción De Datos

Spring Boot

Persistencia Spring Data

Actualizar Datos De Base De Datos

Spring Boot

Persistencia Spring Data

Borrar Datos De Base De Datos

Spring Boot

Persistencia Spring Data

Consultas Jpql Con @Query En Spring Data Jpa

Spring Boot

Persistencia Spring Data

Api Query By Example (Qbe)

Spring Boot

Persistencia Spring Data

Api Specification

Spring Boot

Persistencia Spring Data

Repositorios Reactivos

Spring Boot

Persistencia Spring Data

Introducción E Instalación De Apache Kafka

Spring Boot

Mensajería Asíncrona

Crear Proyecto Con Apache Kafka

Spring Boot

Mensajería Asíncrona

Creación De Producers

Spring Boot

Mensajería Asíncrona

Creación De Consumers

Spring Boot

Mensajería Asíncrona

Kafka Streams En Spring Boot

Spring Boot

Mensajería Asíncrona

Introducción A Spring Webflux

Spring Boot

Reactividad Webflux

Spring Data R2dbc

Spring Boot

Reactividad Webflux

Controlador Rest Reactivo Basado En Anotaciones

Spring Boot

Reactividad Webflux

Controlador Rest Reactivo Funcional

Spring Boot

Reactividad Webflux

Operadores Reactivos Básicos

Spring Boot

Reactividad Webflux

Operadores Reactivos Avanzados

Spring Boot

Reactividad Webflux

Cliente Reactivo Webclient

Spring Boot

Reactividad Webflux

Introducción A Spring Security

Spring Boot

Seguridad Con Spring Security

Seguridad Basada En Formulario En Mvc Con Thymeleaf

Spring Boot

Seguridad Con Spring Security

Registro De Usuarios

Spring Boot

Seguridad Con Spring Security

Login De Usuarios

Spring Boot

Seguridad Con Spring Security

Verificar Token Jwt En Peticiones

Spring Boot

Seguridad Con Spring Security

Seguridad Jwt En Api Rest Spring Web

Spring Boot

Seguridad Con Spring Security

Seguridad Jwt En Api Rest Reactiva Spring Webflux

Spring Boot

Seguridad Con Spring Security

Autenticación Y Autorización Con Anotaciones

Spring Boot

Seguridad Con Spring Security

Testing Unitario De Componentes Y Servicios

Spring Boot

Testing Con Spring Test

Testing De Repositorios Spring Data Jpa

Spring Boot

Testing Con Spring Test

Testing Controladores Spring Mvc Con Thymeleaf

Spring Boot

Testing Con Spring Test

Testing Controladores Rest Con Json

Spring Boot

Testing Con Spring Test

Testing De Aplicaciones Reactivas Webflux

Spring Boot

Testing Con Spring Test

Testing De Seguridad Spring Security

Spring Boot

Testing Con Spring Test

Testing Con Apache Kafka

Spring Boot

Testing Con Spring Test

Integración Con Angular

Spring Boot

Integración Frontend

Integración Con React

Spring Boot

Integración Frontend

Integración Con Vue

Spring Boot

Integración Frontend

Accede GRATIS a SpringBoot y certifícate

En esta lección

Objetivos de aprendizaje de esta lección

  • Crear un API REST reactivo con Spring WebFlux
  • Utilizar controladores funcionales en lugar de basados en anotaciones
  • Aprender las clases de rutas y handlers funcionales
  • Utilizar operadores reactivos