DEV Community

Cover image for Using ResponseEntity with global exception handling in Spring Boot 3
William
William

Posted on

Using ResponseEntity with global exception handling in Spring Boot 3

Now that we already have one REST API working, let's improving with using a better response code and control all exception with a global class.

Create a global class to handle exceptions

First create a record class ExceptionResponse inside the new folder exceptions to return information about errors that occurred.

package com.spring.exceptions;

import java.time.Instant;

public record ExceptionResponse(Instant data, String message, String details) {
}

Enter fullscreen mode Exit fullscreen mode

This record has a variable to return the instant that error occurred, and a message with details.

Now let's create a expeception ResourceNotFoundException
inside the folder exceptions to use when it isn't find a specific entity, in this case a Product.

package com.spring.exceptions;

import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ResponseStatus;

@ResponseStatus(HttpStatus.NOT_FOUND)
public class ResourceNotFoundException extends RuntimeException {
    public ResourceNotFoundException(String messageError) {
        super(messageError);
    }
}
Enter fullscreen mode Exit fullscreen mode

The @ResponseStatus annotation are used to inform the default return code for that exception, in this case NOT_FOUND = 404.

To finish this part, it's necessary create a global class, create a class CustomizedResponseEntityExceptionHandler inside the folder exceptions a new folder handler.

package com.spring.exceptions.handler;

import com.spring.exceptions.ExceptionResponse;
import com.spring.exceptions.ResourceNotFoundException;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.context.request.WebRequest;

import java.time.Instant;

@ControllerAdvice
@RestController
public class CustomizedResponseEntityExceptionHandler {

    @ExceptionHandler(Exception.class)
    public final ResponseEntity<ExceptionResponse> handlerAllExceptions(Exception exception, WebRequest request) {
        ExceptionResponse exceptionResponse = new ExceptionResponse(Instant.now(), exception.getMessage(), request.getDescription(false));
        return new ResponseEntity<>(exceptionResponse, HttpStatus.INTERNAL_SERVER_ERROR);
    }

    @ExceptionHandler(ResourceNotFoundException.class)
    public final ResponseEntity<ExceptionResponse> handlerNotFoundExceptions(Exception exception, WebRequest request) {
        ExceptionResponse exceptionResponse = new ExceptionResponse(Instant.now(), exception.getMessage(), request.getDescription(false));
        return new ResponseEntity<>(exceptionResponse, HttpStatus.NOT_FOUND);
    }

}
Enter fullscreen mode Exit fullscreen mode

This class is using the @ControllerAdvice annotation to deal with all exceptions in the app, it's a way to control in only one place. In this part you decide with is better to control case a case or all in one.
This class is a Controller because use @RestController, and each method with @ExceptionHandler annotation means that when that exception occurs that method will be responsible for the treatment and return.

We are using a global Exception to deal with the global expection and ResourceNotFoundException for the situation mentioned before.

Use ResourceNotFoundException in ProductService

Now let's improve the methods, first with the method findById.

    public Product findById(Long id) {
        return repository.findById(id).orElseThrow(() -> new ResourceNotFoundException("Not found Product with this ID"));
    }
Enter fullscreen mode Exit fullscreen mode

When not found a product with the given ID, exception will be thrown.

About the update and delete, only refractory to use findById.

    @Transactional
    public Product update(Product product, Long id) {
        var productPersisted = findById(id);

        BeanUtils.copyProperties(product, productPersisted, "id");

        return repository.save(productPersisted);
    }

    @Transactional
    public void delete(Long id) {
        var productPersisted = findById(id);
        repository.delete(productPersisted);
    }
Enter fullscreen mode Exit fullscreen mode

Use ResponseEntity in ResponseEntity

All methods need to return a ResponseEntity with the perfect code to that situation, so look the below change.

    @PostMapping(consumes = MediaType.APPLICATION_JSON_VALUE,
            produces = MediaType.APPLICATION_JSON_VALUE)
    public ResponseEntity<Product> create(@RequestBody ProductCreateUpdate productCreate) {
        Product product = new Product();
        BeanUtils.copyProperties(productCreate, product);

        return ResponseEntity.status(HttpStatus.CREATED).body(service.create(product));
    }

    @GetMapping(path = "/{id}", produces = MediaType.APPLICATION_JSON_VALUE)
    public ResponseEntity<Product> findById(@PathVariable(value = "id") Long id) {
        return ResponseEntity.status(HttpStatus.OK).body(service.findById(id));
    }

    @GetMapping(produces = MediaType.APPLICATION_JSON_VALUE)
    public ResponseEntity<List<Product>> findAll() {
        var products = service.findAll();
        if (products.isEmpty()) return ResponseEntity.noContent().build();
        return ResponseEntity.status(HttpStatus.OK).body(products);
    }

    @PutMapping(path = "/{id}",
            consumes = MediaType.APPLICATION_JSON_VALUE,
            produces = MediaType.APPLICATION_JSON_VALUE)
    public ResponseEntity<Product> update(@PathVariable(value = "id") Long id, @RequestBody ProductCreateUpdate productUpdate) {
        Product product = new Product();
        BeanUtils.copyProperties(productUpdate, product);

        return ResponseEntity.status(HttpStatus.OK).body(service.update(product, id));
    }

    @DeleteMapping(path = "/{id}")
    public ResponseEntity<?> delete(@PathVariable(value = "id") Long id) {
        service.delete(id);
        return ResponseEntity.noContent().build();
    }
Enter fullscreen mode Exit fullscreen mode

The ResponseEntity is used to return a entity declare in <>, to return a specific code, use the method .status() with enum HttpStatus, and a body use .body().
The method create will return a code CREATED = 201 and a entity Product.
The method findById and update return OK = 200 with the product, but if not find a Product with that ID, the return is NOT_FOUND.
The method findAll return OK if exist some value, else return NO_CONTENT = 204.
The method delete always return NO_CONTENT because don't have a product, but like the findById and update when not find a product return NOT_FOUND.

Testing

In Postman do this request to see the different codes.

post
Now when the POST localhost:8080/api/product is requested, the code return is 201 Created.


get200
When the GET/PUT localhost:8080/api/product/1 is requested, the response code is 200 OK, but if put a ID that don't exist the return is 404 NOT FOUND.
getnot found


The GET localhost:8080/api/product now return 200 OK when find some product, but if doesn't exist anyone, return 204 No Content.
findall


To finish the DELETE in localhost:8080/api/product/1 return 204 No Content.
delete

Noticed that when a custom exception error ResourceNotFoundException is returned, a return comes as created in the record class ExceptionResponse.

I don't do all possible return because they are like in class ProductController explanation, but feel free to test all.

Conclusion

In this post, we create the global class with @ControllerAdvice annotation to deal with all expectations.
Also improving the service and controller class to use ResponseEntity.

Next Step

In the next step we will create validations in the body of the request received.

Top comments (0)