Spring Boot Exception Handling for REST APIs

In this article, we will learn how to handle exceptions for RESTful Web Services developed using Spring Boot. We can handle exceptions in REST API in the same way we handle them in the Spring MVC-based web application—by using the @ExceptionHandler and @ControllerAdvice annotations. Instead of rendering a view, you can return ResponseEntity with the appropriate HTTP status code and exception details.

By default, Spring Boot provides a /error mapping that handles all errors in a sensible way, and it is registered as a “global” error page in the servlet container. Rest clients, it produces a JSON response with details of the error, the HTTP status, and the exception message.

Instead of simply throwing an exception with the HTTP status code, it is better to provide more details about the issue, such as the error code, message, cause, etc. In this example, we define a class annotated with @ControllerAdvice to customize the JSON document to return for a particular controller and/or exception type.

Let's develop CRUD REST APIs for the Employee resource using Spring Boot, Spring Data JPA (Hibernate), and MySQL database and we will look into exception handling for these REST services.

Table of Contents

  1. What we'll build?
  2. Tools and Technologies Used
  3. Creating and Importing a Project
  4. The pom.xml File
  5. Create JPA Entity - Employee.java
  6. Create Spring JPA Repository - EmployeeRepository.java
  7. Create Spring Rest Controller - EmployeeController.java
  8. Exception(Error) Handling for RESTful Services
  9. Running Application via Application.java
  10. Testing via Postman Client

1. What we'll build?

We will develop a simple Spring Boot RESTful CRUD APIs for Employee resource and we will implement Exception(Error) Handling for these RESTful Services.

2. Tools and Technologies Used

3. Create and Set up Spring Boot Project

There are many ways to create a Spring Boot application. The simplest way is to use Spring Initializr at http://start.spring.io/, which is an online Spring Boot application generator.

4. The pom.xml File

Refer to the below pom.xml for your reference.

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>

	<groupId>net.guides.springboot2</groupId>
	<artifactId>springboot2-jpa-crud-example</artifactId>
	<version>0.0.1-SNAPSHOT</version>
	<packaging>jar</packaging>

	<name>springboot2-jpa-crud-example</name>
	<description>Demo project for Spring Boot</description>

	<parent>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-parent</artifactId>
		<version>3.0.4</version>
		<relativePath /> <!-- lookup parent from repository -->
	</parent>

	<properties>
		<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
		<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
		<java.version>17</java.version>
	</properties>

	<dependencies>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-data-jpa</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-validation</artifactId>
		</dependency>
		<dependency>
			<groupId>com.mysql</groupId>
			<artifactId>mysql-connector-j</artifactId>
			<scope>runtime</scope>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-test</artifactId>
			<scope>test</scope>
		</dependency>
	</dependencies>

	<build>
		<plugins>
			<plugin>
				<groupId>org.springframework.boot</groupId>
				<artifactId>spring-boot-maven-plugin</artifactId>
			</plugin>
		</plugins>
	</build>

</project>

5. Create JPA Entity - Employee.java

import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.Size;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

@Entity
@Table(name = "employees")
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class Employee {
    
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    @Column(name = "first_name", nullable = false)
    @NotEmpty(message = "First name should not be empty")
    private String firstName;
    
    @Column(name = "last_name", nullable = false)
    @NotEmpty(message = "Last name should not be empty")
    private String lastName;
    
    @Column(name = "email", nullable = false, unique = true)
    @NotEmpty(message = "Email should not be empty")
    @Size(max = 100, message = "Email should not be more than 100 characters")
    private String email;
    
    @Column(name = "phone_number")
    private String phoneNumber;
    
    @Column(name = "salary")
    private double salary;
    
    @Column(name = "address")
    private String address;
    
    @Column(name = "designation")
    private String designation;
    
    @Column(name = "status")
    private String status;
    
}

Next, we will create a Spring Data JPA repository - EmployeeRepository.java

6. Create Spring JPA Repository - EmployeeRepository.java

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

@Repository
public interface EmployeeRepository extends JpaRepository<Employee, Long> {

}

Once the entity and repository are in place then we will create Spring Rest Controller - EmployeeController.java

7. Create Spring Rest Controller - EmployeeController.java

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import jakarta.validation.Valid;
import java.util.List;

@RestController
@RequestMapping("/api/employees")
@Validated
public class EmployeeController {
    
    @Autowired
    private EmployeeRepository employeeRepository;
    
    @GetMapping
    public List<Employee> getAllEmployees() {
        return employeeRepository.findAll();
    }
    
    @GetMapping("/{id}")
    public ResponseEntity<Employee> getEmployeeById(@PathVariable(value = "id") Long employeeId) {
        Employee employee = employeeRepository.findById(employeeId)
                .orElseThrow(() -> new ResourceNotFoundException("Employee", "id", employeeId));
        return ResponseEntity.ok().body(employee);
    }
    
    @PostMapping
    public Employee createEmployee(@Valid @RequestBody Employee employee) {
        return employeeRepository.save(employee);
    }
    
    @PutMapping("/{id}")
    public ResponseEntity<Employee> updateEmployee(
            @PathVariable(value = "id") Long employeeId,
            @Valid @RequestBody Employee employeeDetails) {
        Employee employee = employeeRepository.findById(employeeId)
                .orElseThrow(() -> new ResourceNotFoundException("Employee", "id", employeeId));
        employee.setFirstName(employeeDetails.getFirstName());
        employee.setLastName(employeeDetails.getLastName());
        employee.setEmail(employeeDetails.getEmail());
        employee.setPhoneNumber(employeeDetails.getPhoneNumber());
        employee.setSalary(employeeDetails.getSalary());
        employee.setAddress(employeeDetails.getAddress());
        employee.setDesignation(employeeDetails.getDesignation());
        employee.setStatus(employeeDetails.getStatus());
        Employee updatedEmployee = employeeRepository.save(employee);
        return ResponseEntity.ok(updatedEmployee);
    }
    
    @DeleteMapping("/{id}")
    public ResponseEntity<Void> deleteEmployee(@PathVariable(value = "id") Long employeeId) {
        Employee employee = employeeRepository.findById(employeeId)
                .orElseThrow(() -> new ResourceNotFoundException("Employee", "id", employeeId));
        employeeRepository.delete(employee);
        return ResponseEntity.noContent().build();
    }
}

So far we have developed CRUD RESTful APIs for the Employee resource. Now we will look into how to handle exceptions or errors for the above RESTFul APIs.

8. Exception(Error) Handling for RESTful Services

Spring Boot provides a good default implementation for exception handling for RESTful Services.

Let’s quickly look at the default Exception Handling features provided by Spring Boot.

Resource Not Present

        {
            "timestamp": 1512713804164,
            "status": 404,
            "error": "Not Found",
            "message": "No message available",
            "path": "/some-dummy-url"
        }

That's a cool error response. It contains all the details that are typically needed.

What happens when we throw an Exception?

Let’s see what Spring Boot does when an exception is thrown from a Resource. we can specify the Response Status for a specific exception along with the definition of the Exception with @ResponseStatus annotation.

Let's create a ResourceNotFoundException.java class.

Define a class for the error response:

            package com.companyname.springbootcrudrest.exception;

            import org.springframework.http.HttpStatus;
            import org.springframework.web.bind.annotation.ResponseStatus;
            
            @ResponseStatus(value = HttpStatus.NOT_FOUND)
            public class ResourceNotFoundException extends Exception{
            
                private static final long serialVersionUID = 1L;
            
                public ResourceNotFoundException(String message){
                    super(message);
                }
            }

Customizing Error Response Structure

The default error response provided by Spring Boot contains all the details that are typically needed.

However, you might want to create a framework-independent response structure for your organization. In that case, you can define a specific error response structure.

Let’s define a simple error response bean.


package com.companyname.springbootcrudrest.exception;

import java.util.Date;

public class ErrorDetails {
    private Date timestamp;
    private String message;
    private String details;

    public ErrorDetails(Date timestamp, String message, String details) {
        super();
        this.timestamp = timestamp;
        this.message = message;
        this.details = details;
    }

    public Date getTimestamp() {
        return timestamp;
    }

    public String getMessage() {
        return message;
   }

    public String getDetails() {
       return details;
   }
}
        

Create GlobalExceptionHandler class

To use ErrorDetails to return the error response, let’s create a GlobalExceptionHandler class annotated with @ControllerAdvice annotation. This class handles exception-specific and global exceptions in a single place.


package com.companyname.springbootcrudrest.exception;

import java.util.Date;

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.context.request.WebRequest;

@ControllerAdvice
public class GlobalExceptionHandler {
    @ExceptionHandler(ResourceNotFoundException.class)
    public ResponseEntity<?> resourceNotFoundException(ResourceNotFoundException ex, WebRequest request) {
        ErrorDetails errorDetails = new ErrorDetails(new Date(), ex.getMessage(), request.getDescription(false));
        return new ResponseEntity<>(errorDetails, HttpStatus.NOT_FOUND);
    }

    @ExceptionHandler(Exception.class)
    public ResponseEntity<?> globleExcpetionHandler(Exception ex, WebRequest request) {
        ErrorDetails errorDetails = new ErrorDetails(new Date(), ex.getMessage(), request.getDescription(false));
        return new ResponseEntity<>(errorDetails, HttpStatus.INTERNAL_SERVER_ERROR);
    }
}
            

We are done with developing CRUD REST APIs and exception handling for REST APIs. Now it's time to run this Spring boot application.

9. Running Application via Application.java

This spring boot application has an entry point Java class called Application.java with the public static void main(String[] args) method, which you can run to start the application.

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class Application {
    
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

10. Testing via Postman Client

Once the application is up and running, you can use Postman to test the REST APIs. You can perform operations such as GET, POST, PUT, and DELETE on the /api/employees endpoint to verify the functionality and the exception handling.