Control de Respuestas HTTP con ResponseEntity en Spring
Resumen
¿Cómo mejorar los endpoints con ResponseEntity en Spring?
Hablar de un controlador API efectivo es hablar de la gestión adecuada de las respuestas HTTP. Implementar ResponseEntity en Spring no solo mejora la legibilidad del código, sino también la claridad y robustez del API en cuestión. ¿Cómo controlar los llamados y mejorar la respuesta de nuestros endpoints? Continúa leyendo para aprender cómo implementar estas transformaciones con éxito.
¿Qué es ResponseEntity en Spring?
ResponseEntity es una clase en Spring que permite gestionar de una manera más controlada las respuestas de los endpoints de una API. Este control es fundamental para definir el cuerpo y el código de estado de una respuesta HTTP, dando mayor claridad a los usuarios sobre cómo fue manejada su petición.
Control de Respuestas: Utilizando ResponseEntity, puedes indicar exactamente qué se está devolviendo y con qué código de estado (como 200 OK, 201 Created o 404 Not Found).
Manejo de Errores: Te permite definir qué hacer cuando una operación no resulta como se esperaba, mejorando la comunicación con el cliente.
¿Cómo utilizar ResponseEntity con getAll?
Para mejorar nuestras respuestas getAll, comenzaremos reemplazando el retorno de una lista simple por un ResponseEntity. Así controlamos no solo el contenido, sino también el código de respuesta HTTP.
Un Optional representa la posibilidad de que un producto exista, o no, en la base de datos. Este puede ser transformado para responder de manera adecuada según el resultado.
¿Cómo implementar ResponseEntity en Get by Category?
Para obtener productos por categoría, la misma lógica se aplica: en lugar de retornar listas vacías o null, responde con ResponseEntity y el código adecuado.
En el caso del método delete, ResponseEntity nos ayuda a controlar si una eliminación fue exitosa o no, dependiendo de la existencia previa del producto.
Después de aplicar estas mejoras, es crucial verificar el funcionamiento correcto de los endpoints. Postman es una excelente herramienta para realizar pruebas y asegurarse de que cada endpoint devuelve el estado y cuerpo esperados.
Ejemplo de Prueba:
Producto no existente: Consulta un producto que no existe, ahora debería responder con 404 Not Found.
Creación de Producto: Crea un producto y verifica que la respuesta ahora incluye 201 Created, reflejando que el recurso fue exitosamente creado.
Spring, junto con el adecuado uso de ResponseEntity, permite construir APIs más robustas y claras, mejorando así la experiencia del desarrollador y usuario final. No te detengas aquí, sigue mejorando tus servicios y explorando nuevas funcionalidades para tus proyectos.
Uno puede usar la anterior línea como alternativa a las líneas 26, 27 y 28.
Buen aporte, igual lo ignore cuando lo vi hace tiempo porque si no encontraba nada en el @service lanzaba una excepción desde antes de regresar al controlador con un @RestControllerAdvice con un método que me responde un ResponseEntity, pero que bien saberlo para cuando me lleve el opcional hasta el @Controller
Así es Manuel, existen varias maneras de hacerlo y esa que propones es muuuy interesante. Muchas gracias por tu aporte! 🚀
En el método delete, si no quieren usar if y else, también pueden usar el operador ternario:
@DeleteMapping("/delete/{id}")publicResponseEntitydelete(@PathVariable("id") int productId){returnnewResponseEntity(this.productService.delete(productId)?HttpStatus.OK:HttpStatus.NOT_FOUND);}
De hecho, también pensé en eso cuando estaba viendo el código de Alejandro. Gracias por compartir esta opción, que a mi parecer, hace el código más amigable.
excelente Operador Ternario. me parece muy bien
Una observacion, cuando se consulta a la base de datos y se desea obtener una lista de producto por categoria y la categoria no existe entonces se devuelve una lista vacía, entonces, al hacer el map or else nunca entra al or else porque En el Optional si está presenta la lista, solo que está vacía, entonces aunque la categoria no exista siempre devolverá estatus OK -> 200.
Yo lo hice asi:
@GetMapping("/category/{id}")publicResponseEntity<List<Product>>getByCategory(@PathVariable("id") int categoryId){List<Product> products = productService.getByCategory(categoryId).orElse(null);return products !=null&&!products.isEmpty()?newResponseEntity<>(products,HttpStatus.OK):newResponseEntity<List<Product>>(HttpStatus.NOT_FOUND);}
La razón por la que utilizo el Not Found (404) en vez del No Content (204) es porque según su definición:
No Content (204) se utiliza cuando la petición se ha completado con éxito pero su respuesta no tiene ningún contenido.
Not Found (404) se usa cuando el servidor no pudo encontrar el contenido solicitado.
En este caso puntual el recurso que solicitamos no existe y por lo tanto el proceso no finalizó como se esperaba, por lo cual un 404 es lo ideal.
gracias por la respuesta, tiene sentido y me aclara la duda que tenia ya que en algunas partes me han pedido colocar este tipo de respuesta como 204 para diferenciarlas de cuando el request como tal no existe, o el servidor esta caído para mostar mensajes diferentes al usuario desde un front
Estuve probando el GET para categorías inexistentes y pasa ésto:
Genera una respuesta OK y muestra una lista vacía, lo cual no es lo esperado. Modifico el código para preguntar si la lista tiene elementos:
@GetMapping("/category/{categoryId}")publicResponseEntity<List<Product>>getByCategory(@PathVariable("categoryId") int categoryId){return productService.getByCategory(categoryId).map(products ->{if(products.size()>0)returnnewResponseEntity<>(products,HttpStatus.OK);elsereturnnewResponseEntity<List<Product>>(HttpStatus.NOT_FOUND);}).orElse(newResponseEntity<>(HttpStatus.NOT_FOUND));}
Y obtengo en Postman lo esperado: Status Not Found y el cuerpo de la respuesta sin contenido.
Pregunta: ¿Estos ajustes se pueden hacer aquí, o lo ideal es que desde el dominio se controle, o desde la persistencia?
Observación: Estoy casi seguro que el método ++.orElse++ no se ejecuta nunca, ya que cuando no hay productos que cumplan con la categoría va a devolver una lista vacía.
Saludos, esta bueno el curso.
Muchas gracias por tu aporte, Luis! Tienes toda la razón y tu apunte es más que acertado.
Lo ideal es que el ajuste se haga desde el controller porque es quien necesita controlar el tipo de respuesta según lo que resulte. En mi caso lo puse de la siguiente manera haciendo uso del filter (donde filtro solo si la lista no es vacía) antes del map:
@GetMapping("/category/{categoryId}")publicResponseEntity<List<Product>>getByCategory(@PathVariable("categoryId") int categoryId){return productService.getByCategory(categoryId).filter(Predicate.not(List::isEmpty)).map(products ->newResponseEntity<>(products,HttpStatus.OK)).orElse(newResponseEntity<>(HttpStatus.NOT_FOUND));}
Muchas gracias por corregir este problema! 🚀
Resuelto, mi pregunta era la misma. Deberían colocarla entre las principales porque hay otros compañeros que les paso lo mismo.
Me funcionan todos los métodos excepto el getByCategory, cuando no encuentra los productos de una categoría retorna Status 200 OK.
Aquí dejo el código:
@GetMapping("/category/{categoryId}")publicResponseEntity<List<Product>>getByCategory(@PathVariable("categoryId") int categoryId){return productService.getByCategory(categoryId).map(products ->newResponseEntity<>(products,HttpStatus.OK)).orElse(newResponseEntity<>(HttpStatus.NOT_FOUND));}```
Así lo acomodé yo
@GetMapping("/category/{catId}")publicResponseEntity<List<Product>>getByCategory(@PathVariable("catId") int categoryId){ final Optional<List<Product>> products = productService.getByCategory(categoryId);if(products.isPresent()&&!products.get().isEmpty())returnnewResponseEntity<>(products.get(),HttpStatus.OK);returnnewResponseEntity<>(HttpStatus.NOT_FOUND);}```
Bien
ResponseEntity esta muy completo y puedes usarlo de varias formas, explora escribiendo ResponseEntity. + ctrl
@GetMapping no debe responder con NOT_FOUD ya que es confuso, el cliente puede pensar que /{id} no existe y está llamando al endpoint incorrecto, para esto está NO_CONTENT que indica que si se encuentra el recurso /{id} pero no hay un producto con el id proporcionado
Esta fue mi forma para hacer el delete, por si a alguien le ayuda
@DeleteMapping("/{productId}")publicResponseEntity<Void>delete(@PathVariable int productId){ boolean deleted = productService.delete(productId);return deleted ?ResponseEntity.ok().build():ResponseEntity.notFound().build();}
Muchas gracias me sirvio ya que me estaba resaltando ResponseEntity, creo que tiene que llevar obligariamente el tipo
Esta forma que comentas evita que no te resalte el ReponseEntity en le ide como fue en mi caso.
Me gusto la solucion para esta caso.
Justo hice una consulta en la clase anterior, pero después de terminar la clase me parece que tiene mucho sentido diferenciar la creación de la actualización del producto.
Está súper! Solo pondría el @PostMapping al create y el @PutMapping al update.
Realizando las pruebas del endpoint getByCategory, buscando una categoría que no existe, ejemplo /100, está regresando 200 OK con []. Cómo se debe hacer para que regrese 404 Not Found?
Porque con ese código debería de funcionar sin problemas lo que quieres.
Hola Angela. Estuve verificando este método y tienes toda la razón. Cómo la lista es vacía ingresa al map y no va al orElse que controla el 404.
Para modificarlo puse un filter (donde filtro únicamente sí la lista no es vacía) antes del map:
@GetMapping("/category/{categoryId}")publicResponseEntity<List<Product>>getByCategory(@PathVariable("categoryId") int categoryId){return productService.getByCategory(categoryId).filter(Predicate.not(List::isEmpty)).map(products ->newResponseEntity<>(products,HttpStatus.OK)).orElse(newResponseEntity<>(HttpStatus.NOT_FOUND));}
Muchas gracias por reportarlo! 🚀
Estaba probando post asi como en el video pero me responde esto:
//Esto es lo que envio{"name":"Lechuga","categoryId":1,"price":2000.0,"stock":60,"active":true}//Esto me responde{"timestamp":"2021-05-15T13:28:43.885+00:00","status":400,"error":"Bad Request","message":"","path":"/platzi-market/api/products/save"}
No entiendo el porque de quitar los opcional dentro del response entity, alguien que me ilumine?
Porque ya no va a retornar un objeto de tipo Optional sino que un objeto de tipo ResponseEntity. Lo que falta al ya saber que vamos a retornar un ResponseEntity es "sacar" los valores que retorna (desde la clase ProductService) del Optional que trae por defecto, por eso es posible hacer un mapeo al llamar por ejemplo productService.getProduct...
Es una de las mejores clases porque empiezo en Spring, anteriormente hice un proyecto básico, pero no incluía las ResponseEntity, si que son muy utiles!
Una forma un poco más elegante de definir el delete:
@DeleteMapping("/{id}")publicResponseEntitydelete(@PathVariable("id") int productId){return productService.delete(productId)?newResponseEntity<>(HttpStatus.NO_CONTENT):newResponseEntity<>(HttpStatus.NOT_FOUND);}
Otra manera de lo mismo :)
@DeleteMapping("/{id}")publicResponseEntitydelete(@PathVariable("id") int productId){returnnewResponseEntity(productService.delete(productId)?HttpStatus.OK:HttpStatus.NOT_FOUND);}
¿Alguien ha notado que cuando se hace un request que devuelve una lista y esta no tiene elementos, igual devuelve status 200?
Por ejemplo si haces un request a /products/category/583 (la categoría 583 no existe) igual devuelve status 200 y debería devolver 404
Es correcto, me sale lo mismo
Que tal Daniel. Es correcto que te regrese 200, es necesario realizar una validación en el controller, si esta vacía regresas not found.
Las peticiones en spring cómo funcionan? cada petición es única y se atiende de forma personalizada, pero Spring puede atender múltiples de peticiones únicas al mismo tiempo ?, espero pueda darme a entender
Claro, Spring puede atender muchas peticiones de manera concurrente tal cual lo hace un contenedor web normalmente. ¿Cuántas puede procesar? Todo dependerá de la memoria que le asignes al desplegar y OBVIAMENTE la lógica de tu aplicación.
De hecho puedes limitar el número de peticiones concurrentes con el parámetro server.tomcat.max-threads del applications.properties.
Te recomiendo esta breve guía de Spring Boot Actuator.
Entre usar el responseEntity y especificar el status code a retornar, o usar el @ResponseStatus sobre el metodo ¿Que es mejor?
Creo que ya es tema de visibilidad de lectura del code, las dos funcionan para el mismo objetivo, creo y pienso que en el return es mejor visible para la lectura del código porque sabes que estás retornando, por el contrario con la anotación sobre la función no sería tan visible, se entendería pero no sería la mejor forma de leer el code.
En el min 8:39 al probar en postman enviamos como parte del body el ID de la categoría y se guarda correctamente pero el object category retorna con null. He implementado esto en mi proyecto pero necesito que obligatoriamente me retorne el objecto category al guardar. ¿Cómo debería implementar esta funcionalidad si el metodo save del repository siempre retorna null y si tengo por ejemplo 4 relaciones como categoryId?
Nos retorna null debido a que el objeto Product Que está en el ResponseEntity no tiene categoría asignada. Al obtener el producto con de nuevo si nos aparece debido a que se obtiene de la base de datos, que si tiene la categoría. Para resolver eso se debería hacer algo como lo siguiente:
publicResponseEntity<?>save(@RequestBodyProduct product){var savedProduct = productService.save(product); savedProduct.setCategory(//posible método para obtener la categoría por ID);returnnewResponseEntity<>(savedProduct,HttpStatus.CREATED);}
Que no es mala practica agregar la acción en la url de endpoint?