Spring Boot REST API Best Practices - Part 4

Share this post:

In this Spring Boot REST API Best Practices series, we have learned how to implement CRUD operations so far. In this Part-4, we will explore how to implement exception handling for our APIs.

You can find the sample code for this tutorial in this GitHub repository.

As mentioned in the Part-3, if a request handling method in a controller throws an Exception, then Spring Boot will handle it and return the response using its default Exception Handling mechanism.

If all you care about is returning a proper HTTP Status code when an Exception is thrown, you can simply use @ResponseStatus annotation to specify which HTTP Status code should be used instead of default INTERNAL_SERVER_ERROR - 500.

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

@ResponseStatus(HttpStatus.NOT_FOUND)
public class BookmarkNotFoundException extends RuntimeException {
    
}

// --------------------------------------

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

@ResponseStatus(HttpStatus.UNPROCESSABLE_ENTITY)
public class InvalidBookmarkUrlException extends RuntimeException {

}

But most likely, you would like to return a customized error response body with an appropriate HTTP Status Code as the response. So, let’s see what are the different approaches to handle the exceptions to handling Exceptions and returning error responses.

Different approaches to handling exceptions

We can handle exceptions in different ways, and depending on your use-case, you can choose one of the approaches that fit best for you.

Handling exceptions in the controller handler method

This is the best approach if you want at most control over the exception handling logic for a particular API endpoint.

For example, in POST /api/bookmarks API endpoint implementation, if the bookmark URL already exists then, BookmarkService may throw DuplicateBookmarkException. If the bookmark title contains certain blocked words, then BookmarkService may throw BookmarkTitleNotAllowedException. So, if you want to handle all those different exceptions in the controller handler method itself then you can follow this approach.

BookmarkController.java

@RestController
@RequestMapping("/api/bookmarks")
class BookmarkController {
    private final BookmarkService bookmarkService;
    //...
    //...

    @PostMapping
    ResponseEntity<BookmarkDTO> create(@RequestBody @Validated CreateBookmarkRequest request) {
        CreateBookmarkCommand cmd = new CreateBookmarkCommand(
                request.title(),
                request.url()
        );
        try {
            BookmarkDTO bookmark = bookmarkService.create(cmd);
            URI location = ServletUriComponentsBuilder
                    .fromCurrentRequest()
                    .path("/api/bookmarks/{id}")
                    .buildAndExpand(bookmark.id()).toUri();
            return ResponseEntity.created(location).body(bookmark);
        } catch(DuplicateBookmarkException e) {
            return ResponseEntity.status(HttpStatus.BAD_REQUEST).build();
        } catch(BookmarkTitleNotAllowedException e) {
            return ResponseEntity.status(HttpStatus.UNPROCESSABLE_ENTITY).build();
        }
    }
}

Using Controller level @ExceptionHandler

Sometimes we may end up handling the same type of Exceptions in the same manner from multiple API handler methods in a controller. For example, the duplicate url check and title validation logic apply to both Create and Update API endpoints. In such cases, instead of duplicating the try-catch logic in multiple places, we can use Controller level @ExceptionHandler as follows:

@RestController
@RequestMapping("/api/bookmarks")
class BookmarkController {
    private final BookmarkService bookmarkService;
    //...
    //...

    @PostMapping
    ResponseEntity<BookmarkDTO> create(@RequestBody @Validated CreateBookmarkRequest request) {
        CreateBookmarkCommand cmd = new CreateBookmarkCommand(request.title(), request.url());
        BookmarkDTO bookmark = bookmarkService.create(cmd);
        URI location = ServletUriComponentsBuilder
                .fromCurrentRequest()
                .path("/api/bookmarks/{id}")
                .buildAndExpand(bookmark.id()).toUri();
        return ResponseEntity.created(location).body(bookmark);
    }

    @PutMapping("/{id}")
    void update(@PathVariable(name = "id") Long id,
                @RequestBody @Validated UpdateBookmarkRequest request) {
        UpdateBookmarkCommand cmd = new UpdateBookmarkCommand(id, request.title(), request.url());
        bookmarkService.update(cmd);
    }

    @ExceptionHandler(DuplicateBookmarkException.class)
    public ResponseEntity<ApiError> handleDuplicateBookmarkException(DuplicateBookmarkException e) {
        ApiError error = new ApiError(e.getMessage());
        return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(error);
    }

    @ExceptionHandler(BookmarkTitleNotAllowedException.class)
    public ResponseEntity<ApiError> handleBookmarkTitleNotAllowedException(BookmarkTitleNotAllowedException e) {
        ApiError error = new ApiError(e.getMessage());
        return ResponseEntity.status(HttpStatus.UNPROCESSABLE_ENTITY).body(error);
    }
}

In this approach, you don’t have to duplicate the exception handling logic in multiple handler methods in the controller. If BookmarkTitleNotAllowedException or DuplicateBookmarkException is thrown from create(…) or update(…) methods, they will be handled by the respective @ExceptionHandler methods.

You can also handle multiple types of Exceptions in the same ExceptionHandler method using @ExceptionHandler({DuplicateBookmarkException.class, BookmarkTitleNotAllowedException.class}). In this case, the ExceptionHandler method should use the common base Exception class of DuplicateBookmarkException and BookmarkTitleNotAllowedException as a method parameter.

GlobalExceptionHandler using @RestControllerAdvice

In the previous section, we have seen how to use @ExceptionHandler at the Controller level. What if the same type of exceptions may occur in different Controllers, and we want to handle those Exceptions in the same way? In such cases, we can use the Global Exception Handling approach by using @RestControllerAdvice.

Create GlobalExceptionHandler as follows:

package com.sivalabs.bookmarks.api;

@RestControllerAdvice
public class GlobalExceptionHandler {
    
    @ExceptionHandler(DuplicateBookmarkException.class)
    public ResponseEntity<ApiError> handleDuplicateBookmarkException(DuplicateBookmarkException e) {
      ApiError error = new ApiError(e.getMessage());
      return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(error);
    }
  
    @ExceptionHandler(BookmarkTitleNotAllowedException.class)
    public ResponseEntity<ApiError> handleBookmarkTitleNotAllowedException(BookmarkTitleNotAllowedException e) {
      ApiError error = new ApiError(e.getMessage());
      return ResponseEntity.status(HttpStatus.UNPROCESSABLE_ENTITY).body(error);
    }
}

By using ControllerAdvice approach, we don’t have to duplicate the same @ExceptionHandler logic in multiple Controllers.

IMPORTANT

If you have an @ExceptionHandler handling the same Exception in both Controller and GlobalExceptionHandler then Controller level @ExceptionHandler method takes priority.

Spring Boot Error Responses using Problem Details for HTTP APIs

Spring Framework 6 implemented the Problem Details for HTTP APIs specification, (RFC 7807).

Spring Boot 3: Error Responses using Problem Details for HTTP APIs

You can read the Spring Boot 3 : Error Responses using Problem Details for HTTP APIs post to learn how to use ProblemDetails API for handling Exceptions.

We can enable RFC 7807 responses either by adding the property spring.mvc.problemdetails.enabled=true or create a global exception handler using @ControllerAdvice by extending ResponseEntityExceptionHandler.

To quickly demonstrate, here is how you can use ProblemDetails API to return error responses in RFC 7807 format.

@RestControllerAdvice
public class GlobalExceptionHandler extends ResponseEntityExceptionHandler {

    @ExceptionHandler(BookmarkNotFoundException.class)
    ProblemDetail handleBookmarkNotFoundException(BookmarkNotFoundException e) {
        ProblemDetail problemDetail = ProblemDetail.forStatusAndDetail(HttpStatus.NOT_FOUND, e.getMessage());
        problemDetail.setTitle("Bookmark Not Found");
        problemDetail.setType(URI.create("https://api.bookmarks.com/errors/not-found"));
        problemDetail.setProperty("errorCategory", "Generic");
        problemDetail.setProperty("timestamp", Instant.now());
        return problemDetail;
    }
}

Now when an unhandled BookmarkNotFoundException is thrown, the following response will be returned:

{
  "type": "https://api.bookmarks.com/errors/not-found",
  "title": "Bookmark Not Found",
  "status": 404,
  "detail": "Bookmark with id=111 not found",
  "instance": "/api/bookmarks/111",
  "errorCategory": "Generic",
  "timestamp": "2023-08-30T05:21:59.828411Z"
}

By extending ResponseEntityExceptionHandler, you can leverage the Spring’s default Exception handling for various common exceptions such as MethodArgumentNotValidException, BindException, MissingServletRequestParameterException, etc. If you want to customize the exception handling for any of those Exceptions, then you can override those respective methods and implement your own logic.

Using Error Handling Spring Boot Starter

We can use Error Handling Spring Boot Starter that can handle Exceptions and return meaningful error responses without having to write custom code.

Add the following library dependency to your pom.xml:

<properties>
  <error-handling-spring-boot-starter.version>4.2.0</error-handling-spring-boot-starter.version>
</properties>

<dependency>
    <groupId>io.github.wimdeblauwe</groupId>
    <artifactId>error-handling-spring-boot-starter</artifactId>
    <version>${error-handling-spring-boot-starter.version}</version>
</dependency>

Now if you try to invoke POST /api/bookmarks API endpoint without providing title and url in the request payload:

curl --location 'http://localhost:8080/api/bookmarks' \
--header 'Content-Type: application/json' \
--data '{}'

Then you will get the following error response:

{
    "code": "VALIDATION_FAILED",
    "message": "Validation failed for object='createBookmarkRequest'. Error count: 2",
    "fieldErrors": [
        {
            "code": "REQUIRED_NOT_EMPTY",
            "message": "URL is required",
            "property": "url",
            "rejectedValue": null,
            "path": "url"
        },
        {
            "code": "REQUIRED_NOT_EMPTY",
            "message": "Title is required",
            "property": "title",
            "rejectedValue": null,
            "path": "title"
        }
    ]
}

You can read the documentation to learn more about Error Handling Spring Boot Starter.

Using Zalando’s problem-spring-web library

Another popular library that can handle Exceptions and return error responses in RFC 7807 format is problem-spring-web created by Zalando.

You can learn how to use problem-spring-web library by watching my Spring Boot Tips : Part 7 - Exception Handling in SpringBoot REST APIs using problem-spring-web video.

You can find the sample code for this tutorial in this GitHub repository.

Spring Boot Tutorials

You can find more Spring Boot tutorials on Spring Boot Tutorials page.

Summary

In this final part of Spring Boot REST API Best Practices series, we have explored how to implement exception handling using different approaches.

I hope this series is helpful in understanding how to implement Spring Boot REST APIs following some best practices.

Share this post:

Related content

comments powered by Disqus