Spring Boot CRUD REST API Exception Handling with @RestControllerAdvice

In this tutorial, we will build a Spring Boot CRUD (Create, Read, Update, Delete) application using H2 in-memory database. We will handle exceptions at the service layer and use @RestControllerAdvice for global exception handling.

What You’ll Learn:

  • Setting up a Spring Boot project.
  • Configuring Spring Boot to use H2 as an in-memory database.
  • Implementing CRUD operations for the Todo entity.
  • Handling exceptions in the service layer.
  • Creating a global exception handler using @RestControllerAdvice.
  • Testing exception handling in REST APIs using Postman.
Spring Boot CRUD REST API Exception Handling

Quick Overview of @RestControllerAdvice annotation

The @RestControllerAdvice annotation in Spring is used to handle exceptions globally for RESTful web services. It combines @ControllerAdvice and @ResponseBody, meaning it intercepts exceptions thrown by any controller and provides custom error handling, automatically returning responses (like JSON) to the client. This makes error management centralized and easier to maintain in a REST API.

Prerequisites

Before starting, ensure you have:

  • Java Development Kit (JDK) 17 or later
  • Apache Maven (for project management)
  • IDE (e.g., IntelliJ IDEA, Eclipse, or VS Code)
  • Postman (to test APIs)

Step 1: Setting Up the Project

1.1 Create a Spring Boot Project

  1. Open Spring Initializr.

  2. Configure the project metadata:

    • Project: Maven
    • Language: Java
    • Spring Boot Version: Latest (3.x)
    • Group: com.example
    • Artifact: spring-boot-h2-crud
    • Java Version: 17 or later
  3. Add the following dependencies:

    • Spring Web: For building RESTful web services.
    • Spring Data JPA: For interacting with the H2 database.
    • H2 Database: In-memory database.
  4. Click Generate to download the project, extract the zip file, and open it in your IDE.

Step 2: Configuring H2 Database

2.1 Configure application.properties

In the src/main/resources/application.properties file, configure Spring Boot to use H2:

# H2 Database Configuration
spring.datasource.url=jdbc:h2:mem:testdb
spring.datasource.driverClassName=org.h2.Driver
spring.datasource.username=sa
spring.datasource.password=password
spring.h2.console.enabled=true
spring.jpa.hibernate.ddl-auto=update

Explanation:

  • spring.datasource.url: Specifies the JDBC URL to use an in-memory H2 database.
  • spring.h2.console.enabled=true: Enables the H2 web console for viewing the database at http://localhost:8080/h2-console.

Step 3: Creating the Todo Entity

3.1 Create the Todo Entity

In the model package, create a class named Todo to represent the entity in the H2 database:

package com.example.springbooth2crud.model;

import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import java.time.LocalDate;

@Entity
public class Todo {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String title;
    private String description;
    private String status;
    private LocalDate createDate;

    // Getters and Setters
    public Long getId() { return id; }
    public void setId(Long id) { this.id = id; }

    public String getTitle() { return title; }
    public void setTitle(String title) { this.title = title; }

    public String getDescription() { return description; }
    public void setDescription(String description) { this.description = description; }

    public String getStatus() { return status; }
    public void setStatus(String status) { this.status = status; }

    public LocalDate getCreateDate() { return createDate; }
    public void setCreateDate(LocalDate createDate) { this.createDate = createDate; }
}

Explanation:

  • @Entity: Marks this class as a JPA entity that will map to the database table.
  • @Id: Specifies the primary key (id).
  • Fields: title, description, status, and createDate represent the attributes of the Todo entity.

Step 4: Creating the Repository

4.1 Create TodoRepository

In the repository package, create an interface TodoRepository that extends JpaRepository:

package com.example.springbooth2crud.repository;

import com.example.springbooth2crud.model.Todo;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

@Repository
public interface TodoRepository extends JpaRepository<Todo, Long> {
}

Explanation:

  • JpaRepository: Provides CRUD operations without writing SQL queries.
  • @Repository: Marks this interface as a repository to perform database operations.

Step 5: Creating the Service Layer

5.1 Create TodoService Interface

In the service package, define the TodoService interface:

package com.example.springbooth2crud.service;

import com.example.springbooth2crud.model.Todo;
import java.util.List;
import java.util.Optional;

public interface TodoService {
    List<Todo> getAllTodos();
    Optional<Todo> getTodoById(Long id);
    Todo saveTodo(Todo todo);
    Todo updateTodo(Long id, Todo todo);
    void deleteTodoById(Long id);
}

5.2 Implement TodoService in TodoServiceImpl

In the service implementation, we'll also throw exceptions from the service layer:

package com.example.springbooth2crud.service;

import com.example.springbooth2crud.exception.ResourceNotFoundException;
import com.example.springbooth2crud.model.Todo;
import com.example.springbooth2crud.repository.TodoRepository;
import org.springframework.stereotype.Service;

import java.util.List;
import java.util.Optional;

@Service
public class TodoServiceImpl implements TodoService {

    private final TodoRepository todoRepository;

    public TodoServiceImpl(TodoRepository todoRepository) {
        this.todoRepository = todoRepository;
    }

    @Override
    public List<Todo> getAllTodos() {
        return todoRepository.findAll();
    }

    @Override
    public Optional<Todo> getTodoById(Long id) {
        return todoRepository.findById(id)
            .orElseThrow(() -> new ResourceNotFoundException("Todo not found with id: " + id));
    }

    @Override
    public Todo saveTodo(Todo todo) {
        return todoRepository.save(todo);
    }

    @Override
    public Todo updateTodo(Long id, Todo todoDetails) {
        Todo todo = todoRepository.findById(id)
            .orElseThrow(() -> new ResourceNotFoundException("Todo not found with id: " + id));
        todo.setTitle(todoDetails.getTitle());
        todo.setDescription(todoDetails.getDescription());
        todo.setStatus(todoDetails.getStatus());
        todo.setCreateDate(todoDetails.getCreateDate());
        return todoRepository.save(todo);
    }

    @Override
    public void deleteTodoById(Long id) {
        Todo todo = todoRepository.findById(id)
            .orElseThrow(() -> new ResourceNotFoundException("Todo not found with id: " + id));
        todoRepository.delete(todo);
    }
}

Explanation:

  • The service layer contains business logic, including CRUD operations and exception handling.
  • ResourceNotFoundException is thrown if the requested Todo item does not exist.

Step 6: Defining Custom Exceptions

6.1 Create ResourceNotFoundException

In the exception package, create a custom exception class:

package com.example.springbooth2crud.exception;

public class ResourceNotFoundException extends RuntimeException {
    public ResourceNotFoundException(String message) {
        super(message);
    }
}

Explanation:

  • This custom exception is thrown when a resource (like a Todo) is not found in the database.

Step 7: Global Exception Handling with @RestControllerAdvice

7.1 Create GlobalExceptionHandler

In the exception package, create a global exception handler using @RestControllerAdvice:

package com.example.springbooth2crud.exception;

import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

import java.time.LocalDateTime;
import java.util.HashMap;
import java.util.Map;

@RestControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(ResourceNotFoundException.class)
    public ResponseEntity<Map<String, Object>> handleResourceNotFoundException(ResourceNotFoundException ex) {
        Map<String, Object> errorDetails = new HashMap<>();
        errorDetails.put("timestamp", LocalDateTime.now());
        errorDetails.put("message", ex.getMessage());
        errorDetails.put("status", HttpStatus.NOT_FOUND.value());

        return new ResponseEntity<>(errorDetails, HttpStatus.NOT_FOUND);
    }

    @ExceptionHandler(Exception.class)
    public ResponseEntity<Map<String, Object>> handleGlobalException(Exception ex) {
        Map<String, Object> errorDetails = new HashMap<>();
        errorDetails.put("timestamp", LocalDateTime.now());
        errorDetails.put("message", "An unexpected error occurred");
        errorDetails.put("status", HttpStatus.INTERNAL_SERVER_ERROR.value());

        return new ResponseEntity<>(errorDetails, HttpStatus.INTERNAL_SERVER_ERROR);
    }
}

Explanation:

  • @RestControllerAdvice handles exceptions globally and sends a structured error response with timestamp, message, and status.

Step 8: Creating the REST Controller

8.1 Create TodoController

In the controller package, create the TodoController to handle REST API requests:

package com.example.springbooth2crud.controller;

import com.example.springbooth2crud.model.Todo;
import com.example.springbooth2crud.service.TodoService;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.util.List;

@RestController
@RequestMapping("/api/todos")
public class TodoController {

    private final TodoService todoService;

    public TodoController(TodoService todoService) {
        this.todoService = todoService;
    }

    @GetMapping
    public List<Todo> getAllTodos() {
        return todoService.getAllTodos();
    }

    @GetMapping("/{id}")
    public ResponseEntity<Todo> getTodoById(@PathVariable Long id) {
        return ResponseEntity.ok(todoService.getTodoById(id).get());
    }

    @PostMapping
    public ResponseEntity<Todo> createTodo(@RequestBody Todo todo) {
        Todo savedTodo = todoService.saveTodo(todo);
        return new ResponseEntity<>(savedTodo, HttpStatus.CREATED);
    }

    @PutMapping("/{id}")
    public ResponseEntity<Todo> updateTodo(@PathVariable Long id, @RequestBody Todo todoDetails) {
        Todo updatedTodo = todoService.updateTodo(id, todoDetails);
        return ResponseEntity.ok(updatedTodo);
    }

    @DeleteMapping("/{id}")
    public ResponseEntity<Void> deleteTodoById(@PathVariable Long id) {
        todoService.deleteTodoById(id);
        return ResponseEntity.noContent().build();
    }
}

Step 9: Running and Testing the Application

9.1 Running the Application

To run the application, open SpringBootH2CrudApplication.java and click the Run button in your IDE, or run the following command in the terminal:

./mvnw spring-boot:run

9.2 Testing with Postman

Test the REST APIs using Postman:

  • GET all todos:

    • URL: http://localhost:8080/api/todos
    • Response: A list of todos.
  • GET todo by ID:

    • URL: http://localhost:8080/api/todos/{id}
    • Response: Todo details or 404 Not Found.
  • POST create a new todo:

    • URL: http://localhost:8080/api/todos
    • Body:
      {
        "title": "Learn Spring Boot",
        "description": "Study Spring Boot CRUD application with H2.",
        "status": "IN_PROGRESS",
        "createDate": "2024-09-22"
      }
      
  • PUT update a todo:

    • URL: http://localhost:8080/api/todos/{id}
    • Body:
      {
        "title": "Complete Spring Boot Project",
        "description": "Update the CRUD project with H2 and exception handling.",
        "status": "COMPLETED",
        "createDate": "2024-09-22"
      }
      
  • DELETE a todo:

    • URL: http://localhost:8080/api/todos/{id}
    • Response: 204 No Content.

Conclusion

In this tutorial, we built a Spring Boot CRUD REST API with H2 Database, handled exceptions globally using @RestControllerAdvice, and tested the endpoints using Postman. We followed best practices by throwing exceptions in the service layer and returning meaningful error messages.

This guide provides a clean and maintainable way to implement a CRUD application with proper exception handling in Spring Boot.

Comments