Using ResponseEntity with global exception handling in Spring Boot 3
Tags: java, springboot, restapi, spring
Published: 2024-01-19 01:48:35

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) {
}

```
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);
    }
}
```
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);
    }

}
```
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"));
    }
```
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);
    }
```
## 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();
    }
```
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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/7zc4e1hha6k54bsnz638.png)
Now when the **POST localhost:8080/api/product** is requested, the code return is **201 Created**.

---

![get200](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/5hgbr337dkoebckma3o7.png)
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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/a6vikd1svuf13ggg9u7a.png)

---

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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/z0a7eylzcacklcxuzzuj.png)

---
To finish the **DELETE** in **localhost:8080/api/product/**1 return **204 No Content**.
![delete](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/zuk37k6suxq07q0dbzco.png)

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.