Manuel Eguiluz
Alejandro Rico García
Alejandro Ramírez
Profesor¿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.
ResponseEntity, puedes indicar exactamente qué se está devolviendo y con qué código de estado (como 200 OK, 201 Created o 404 Not Found).¿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.
List<Product> products = productRepository.findAll(); return new ResponseEntity<>(products, HttpStatus.OK);
¿Cómo manejar un optional con ResponseEntity?
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.
return productService.getProduct(id) .map(product -> new ResponseEntity<>(product, HttpStatus.OK)) .orElse(new ResponseEntity<>(HttpStatus.NOT_FOUND));
¿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.
List<Product> productsByCategory = productService.getByCategory(categoryId); return new ResponseEntity<>(!productsByCategory.isEmpty() ? productsByCategory : HttpStatus.NOT_FOUND);
¿Qué mejoras aporta ResponseEntity al método Save?
El método save puede reflejar correctamente cuándo un producto fue creado con éxito usando el código de estado 201 Created.
Product product = productService.save(newProduct); return new ResponseEntity<>(product, HttpStatus.CREATED);
¿Cómo gestionar el delete de forma eficiente?
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.
boolean deleted = productService.delete(id); return new ResponseEntity<>(deleted ? HttpStatus.OK : HttpStatus.NOT_FOUND);
¿Cómo verificar los cambios en Postman?
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:
404 Not Found.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.
Manuel Eguiluz
Alejandro Rico García
Alejandro Ramírez
ProfesorAlben Daniel Bustamante Barrios
Hiram Rodriguez Gomez
Jorge Martinez
Luis Márquez
John Steven Bernal Gonzalez
Rolando Mamani Salas
Dianela Tamayo
Alejandro Ramírez
ProfesorDianela Tamayo
Luis Enrique Carreño Herreño
Alejandro Ramírez
ProfesorJosé Alejandro Maya Montiel
José Carlos Fernández Bermejo
Pablo Ezequiel Seitler
Rolando Mamani Salas
Alejandro Rico García
Carlos Andres Valencia Rojas
Daniel Felipe Walteros Trujillo
John Steven Bernal Gonzalez
Robert Báez
Rosalí Zaracho
Alejandro Ramírez
ProfesorAngela Espinosa
Alejandro Ramírez
ProfesorAlejandro Ramírez
ProfesorMario Tepe
Andrés Leonardo Caro
David Barrera
Carlos Jose Hernandez Candray
Felipe Hernández
Esteban Fleitas
Daniel Ernesto Aguilar Lopez
José Alejandro Maya Montiel
Felipe de Jesús Galindo Aguilar
Alejandro Rico García
Alejandro Ramírez
ProfesorSebastián Andrade
Daniel Castillo Cifuentes
Luis Pillaga
Eduard Arias
Diego Serrano
Desde la versión 5 de Spring existe el siguiente método static en ResponseEntity.
ResponseEntity.of(productService.getProduct(productId))
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}") public ResponseEntity delete(@PathVariable("id") int productId) { return new ResponseEntity(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}") public ResponseEntity<List<Product>> getByCategory(@PathVariable("id") int categoryId){ List<Product> products = productService.getByCategory(categoryId).orElse(null); return products != null && !products.isEmpty() ? new ResponseEntity<>(products, HttpStatus.OK) : new ResponseEntity<List<Product>>(HttpStatus.NOT_FOUND); }
Muchas gracias!
Disculpa Luis, ¿en qué casos daría null List<Product> products?
porque un not found en vez de un no content???
La razón por la que utilizo el Not Found (404) en vez del No Content (204) es porque según su definición:
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:
@GetMapping("/category/{categoryId}") public ResponseEntity<List<Product>> getByCategory(@PathVariable("categoryId") int categoryId) { return productService.getByCategory(categoryId) .map(products -> { if (products.size() > 0) return new ResponseEntity<>(products, HttpStatus.OK); else return new ResponseEntity<List<Product>>(HttpStatus.NOT_FOUND); }) .orElse(new ResponseEntity<>(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}") public ResponseEntity<List<Product>> getByCategory(@PathVariable("categoryId") int categoryId) { return productService.getByCategory(categoryId).filter(Predicate.not(List::isEmpty)) .map(products -> new ResponseEntity<>(products, HttpStatus.OK)) .orElse(new ResponseEntity<>(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}") public ResponseEntity<List<Product>> getByCategory(@PathVariable("categoryId") int categoryId){ return productService.getByCategory(categoryId) .map(products -> new ResponseEntity<>(products,HttpStatus.OK)) .orElse(new ResponseEntity<>(HttpStatus.NOT_FOUND)); }```
Así lo acomodé yo
@GetMapping("/category/{catId}") public ResponseEntity<List<Product>> getByCategory(@PathVariable("catId") int categoryId) { final Optional<List<Product>> products = productService.getByCategory(categoryId); if (products.isPresent() && !products.get().isEmpty()) return new ResponseEntity<>(products.get(), HttpStatus.OK); return new ResponseEntity<>(HttpStatus.NOT_FOUND); }```
Bien
ResponseEntity esta muy completo y puedes usarlo de varias formas, explora escribiendo ResponseEntity. + ctrl
return ResponseEntity.of(Optional.ofNullable(productService.getAll())); return ResponseEntity.ok().body(productService.getAll()); return ResponseEntity.status(HttpStatus.OK).body(productService.getAll()); return new ResponseEntity<>(productService.getAll(), HttpStatus.OK);
En lo personal me gusta más esta ya que te responde un HttpStatus y el body
return ResponseEntity.status(HttpStatus.OK).body(productService.getAll());
@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}") public ResponseEntity<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.
@PutMapping() public ResponseEntity<Product> create(@RequestBody Product product) { return new ResponseEntity<>(productService.save(product), HttpStatus.CREATED); } @PostMapping() public ResponseEntity<Product> update(@RequestBody Product product) { return new ResponseEntity<>(productService.save(product), HttpStatus.OK); }
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?
Angela, el código que estás utilizando es este
@GetMapping("/category/{categoryId}") public ResponseEntity<List<Product>> getByCategory(@PathVariable("categoryId") int categoryId) { return productService.getByCategory(categoryId) .map(products -> new ResponseEntity<>(products, HttpStatus.OK)) .orElse(new ResponseEntity<>(HttpStatus.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}") public ResponseEntity<List<Product>> getByCategory(@PathVariable("categoryId") int categoryId) { return productService.getByCategory(categoryId).filter(Predicate.not(List::isEmpty)) .map(products -> new ResponseEntity<>(products, HttpStatus.OK)) .orElse(new ResponseEntity<>(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}") public ResponseEntity delete(@PathVariable("id") int productId) { return productService.delete(productId) ? new ResponseEntity<>(HttpStatus.NO_CONTENT) : new ResponseEntity<>(HttpStatus.NOT_FOUND); }
Otra manera de lo mismo :)
@DeleteMapping("/{id}") public ResponseEntity delete(@PathVariable("id") int productId) { return new ResponseEntity(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:
public ResponseEntity<?> save(@RequestBody Product product) { var savedProduct = productService.save(product); savedProduct.setCategory(//posible método para obtener la categoría por ID); return new ResponseEntity<>(savedProduct, HttpStatus.CREATED); }
Que no es mala practica agregar la acción en la url de endpoint?