Enterprise Java

Spring Boot MVC REST Controller Example & Unit Tests

In modern web development, REST APIs are essential for enabling communication between different systems and platforms. The Spring Framework, particularly Spring Boot, provides robust support for building RESTful web services, simplifying the development process through various annotations and tools. One such powerful annotation is @RestController, which is used in Spring MVC to create RESTful API controllers. Let us delve into understanding a Spring Boot REST controller example to illustrate how to build a RESTful web service using Spring Boot.

1. What is @RestController annotation?

The @RestController annotation in Spring Boot is a specialized version of the @Controller annotation. It is typically used in web applications to handle RESTful web services. When a class is annotated with @RestController, it serves as a convenience annotation that combines @Controller and @ResponseBody. This means that the methods in the class will return data directly in the response body, rather than rendering a view. It is particularly useful for creating RESTful APIs, as it simplifies the development process by eliminating the need for explicit @ResponseBody annotations on each method. Overall, @RestController streamlines the development of RESTful web services by providing a clear and concise way to define controllers that return JSON or XML responses.

2. Setting up a database on Docker

Usually, setting up the database is a tedious step but with Docker, it is a simple process. You can watch the video available at this link to understand the Docker installation on Windows OS. Once done open the terminal and trigger the below command to set and run postgresql.

-- Remember to change the password --
docker run -d -p 5432:5432 -e POSTGRES_PASSWORD= --name postgres postgres

-- command to stop the Postgres docker container --
docker stop postgres

-- command to remove the Postgres docker container --
docker rm postgres

Remember to enter the password of your choice. If everything goes well the postgresql database server will be up and running on a port number – 5432 and you can connect with the Dbeaver GUI tool for connecting to the server.

Fig. 1. Postgres on Docker

2.1 Setting up pre-requisite data

To proceed further with the tutorial we will set up the required mock data in the postgresql.

drop database mydatabase;

create database mydatabase;

drop table books;

create table books (
    id bigserial primary key,
    title varchar(255) not null,
    author varchar(255) not null,
    price double precision not null
);

insert into books (title, author, price) values ('the great gatsby', 'f. scott fitzgerald', 10.99);
insert into books (title, author, price) values ('1984', 'george orwell', 8.99);
insert into books (title, author, price) values ('to kill a mockingbird', 'harper lee', 12.50);
insert into books (title, author, price) values ('pride and prejudice', 'jane austen', 7.95);
insert into books (title, author, price) values ('the catcher in the rye', 'j.d. salinger', 9.99);

select * from books;
Fig. 2: Sample data

3. Code Example

3.1 Dependencies

Add the following dependencies to your build.gradle file or if you have created a spring project from start.spring.io this won’t be necessary as the file will be automatically populated with the dependencies information.

plugins {
    id 'java'
    id 'org.springframework.boot' version '3.3.0'
    id 'io.spring.dependency-management' version '1.1.5'
}

group = 'jcg'
version = '0.0.1-SNAPSHOT'

java {
    toolchain {
        languageVersion = JavaLanguageVersion.of(17)
    }
}

repositories {
    mavenCentral()
}

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
    implementation 'org.springframework.boot:spring-boot-starter-validation'
    implementation 'org.springframework.boot:spring-boot-starter-web'
    runtimeOnly 'org.postgresql:postgresql'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
    testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}

tasks.named('test') {
    useJUnitPlatform()
}

3.2 Configure application and database properties

Add the following properties to the application.properties file present in the resources folder.

# application name
spring.application.name=springjpademo

# database properties
spring.datasource.url=jdbc:postgresql://localhost:5432/mydatabase
spring.datasource.username=postgres
spring.datasource.password=somepostgrespassword

# application properties
server.port=9090
spring.main.banner-mode=off
spring.main.log-startup-info=false

The properties file defines:

  • Application Name:
    • spring.application.name=springjpademo – Sets the name of the Spring application to “springjpademo”.
  • Database Properties:
    • spring.datasource.url=jdbc:postgresql://localhost:5432/mydatabase – Configures the JDBC URL for connecting to a PostgreSQL database named “mydatabase” running on localhost.
    • spring.datasource.username=postgres – Specifies the username for connecting to the PostgreSQL database.
    • spring.datasource.password=somepostgrespassword – Specifies the password associated with the username for database authentication.
  • Application Properties:
    • server.port=9090 – Sets the port number on which the Spring Boot application will run.
    • spring.main.banner-mode=off – Disables the startup banner that is normally displayed when the application starts.
    • spring.main.log-startup-info=false – Prevents logging of startup information such as application configuration details.

3.3 Creating the Model classes

Create a Book entity class to interact with the JpaRepository interface and perform the CRUD operations.

@Entity
@Table(name = "books")
public class Book {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;

    @NotEmpty(message = "Title is required")
    private String title;

    @NotEmpty(message = "Author is required")
    private String author;

    @NotNull(message = "Price is required")
    private Double price;

    // toString, Getters, and Setters methods
}

We’ve added some basic validation to our Book entity using annotations like @NotEmpty and @NotNull.

3.4 Creating the Data interaction layer

Spring Data JPA builds on top of JPA and provides an interface, JpaRepository, that offers a range of out-of-the-box CRUD operations and the ability to define custom queries. By extending this interface, developers can create repositories for their domain entities without having to write detailed data access codes. This approach not only accelerates development but also ensures cleaner and more maintainable code.

Create the book repository interface to interact with the book entity for interacting with the SQL table via the JpaRepository interface and perform the CRUD operations.

@Repository
public interface BookRepository extends JpaRepository<Book, Long> {
}

The @Repository annotation marks the BookRepository interface as a Spring Data repository, which is a specialized component used for data access and management. This interface extends JpaRepository<Book, Long>, where Book is the entity type that this repository manages, and Long is the type of the entity’s primary key. By extending JpaRepository, the BookRepository interface inherits a variety of methods for performing common database operations such as saving, finding, and deleting entities. This setup allows you to interact with the database without having to write boilerplate code for these operations. The BookRepository interface will be automatically implemented by Spring Data JPA, providing the necessary data access logic at runtime.

3.5 Create the Controller file

Now, let’s create a REST controller to handle CRUD operations for our Book resource:

// Note- Skipping the service layer for brevity.
@RestController
@RequestMapping("/api/books")
public class BookController {

    @Autowired
    private BookRepository bookRepository;

    @GetMapping
    public List<Book> getAllBooks() {
        return bookRepository.findAll();
    }

    @GetMapping("/{id}")
    public ResponseEntity<Book> getBookById(@PathVariable Long id) {
        Optional<Book> book = bookRepository.findById(id);
        return book.map(ResponseEntity::ok).orElseGet(() -> ResponseEntity.notFound().build());
    }

    @PostMapping
    public Book createBook(@Valid @RequestBody Book book) {
        return bookRepository.save(book);
    }

    @PutMapping("/{id}")
    public ResponseEntity<Book> updateBook(@PathVariable Long id, @Valid @RequestBody Book bookDetails) {
        Optional<Book> book = bookRepository.findById(id);
        if (book.isPresent()) {
            Book existingBook = book.get();
            existingBook.setTitle(bookDetails.getTitle());
            existingBook.setAuthor(bookDetails.getAuthor());
            existingBook.setPrice(bookDetails.getPrice());
            return ResponseEntity.ok(bookRepository.save(existingBook));
        } else {
            return ResponseEntity.notFound().build();
        }
    }

    @DeleteMapping("/{id}")
    public ResponseEntity<Book> deleteBook(@PathVariable Long id) {
        Optional<Book> book = bookRepository.findById(id);
        if (book.isPresent()) {
            bookRepository.delete(book.get());
            return ResponseEntity.noContent().build();
        } else {
            return ResponseEntity.notFound().build();
        }
    }
}

The code defines:

  • @RestController: Marks the BookController class as a RESTful controller in a Spring application, allowing it to handle HTTP requests.
  • @RequestMapping(“/api/books”): Sets the base URL path for all endpoints in this controller to /api/books.
  • @Autowired private BookRepository bookRepository: Autowires (injects) an instance of BookRepository into the controller, enabling data access operations.
  • @GetMapping: Handles HTTP GET requests to retrieve all books from the database.
    • Returns a list of all books by calling bookRepository.findAll().
  • @GetMapping(“/{id}”): Handles HTTP GET requests to retrieve a book by its ID.
    • Uses @PathVariable Long id to capture the book ID from the URL.
    • Attempts to find the book using bookRepository.findById(id).
    • Returns ResponseEntity.ok(book) if the book is found, or ResponseEntity.notFound().build() if not.
  • @PostMapping: Handles HTTP POST requests to create a new book.
    • Validates the incoming request body (@Valid @RequestBody Book book) against validation rules defined in the Book class.
    • Saves the new book to the database using bookRepository.save(book).
    • Returns the saved book object.
  • @PutMapping(“/{id}”): Handles HTTP PUT requests to update an existing book.
    • Similar to @GetMapping("/{id}"), it captures the book ID and attempts to find the existing book.
    • If the book exists, updates its details with those from @Valid @RequestBody Book bookDetails.
    • Returns ResponseEntity.ok(bookRepository.save(existingBook)) with the updated book if successful, or ResponseEntity.notFound().build() if the book does not exist.
  • @DeleteMapping(“/{id}”): Handles HTTP DELETE requests to delete a book by its ID.
    • Attempts to find the book by ID and deletes it using bookRepository.delete(book.get()) if found.
    • Returns ResponseEntity.noContent().build() indicating successful deletion, or ResponseEntity.notFound().build() if the book does not exist.

3.6 Create the Error handling file

To handle errors gracefully, we’ll create a global exception handler:

// To handle errors gracefully we are creating this global exception handler
@RestControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseEntity<Map<String, String>> handleValidationExceptions(MethodArgumentNotValidException ex) {
        Map<String, String> errors = new HashMap();
        ex.getBindingResult().getFieldErrors().forEach(error -> errors.put(error.getField(), error.getDefaultMessage()));
        return new ResponseEntity(errors, HttpStatus.BAD_REQUEST);
    }
}

The code defines:

  • @RestControllerAdvice: Indicates that the GlobalExceptionHandler class is a global exception handler for the entire Spring application, providing centralized error handling for all controllers.
  • @ExceptionHandler(MethodArgumentNotValidException.class): Specifies that the handleValidationExceptions method will handle exceptions of type MethodArgumentNotValidException.
  • handleValidationExceptions(MethodArgumentNotValidException ex): Method that handles validation exceptions thrown during request processing.
    • Creates a new HashMap called errors to store field errors.
    • Uses ex.getBindingResult().getFieldErrors() to iterate through field errors and populate the errors map with field names as keys and error messages as values.
    • Returns a ResponseEntity containing the errors map and HTTP status HttpStatus.BAD_REQUEST (400), indicating that the request was malformed or contained invalid data.

3.7 Create the Main file

Create a Spring boot application to initialize the application and hit the controller endpoints.

@SpringBootApplication
public class MvccrudexampleApplication {

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

3.8 Run the application

Run your Spring Boot application and the application will be started on a port number specified in the application properties file. As soon as the application is started the application endpoints will be initialized and you can use the endpoints to interact with the database to fetch the details.

-- GET http://localhost:9090/api/books: Retrieves all books.

-- GET http://localhost:9090/api/books/{{id}}: Retrieves a specific book by ID.

-- POST http://localhost:9090/api/books: Creates a new book.

-- POST http://localhost:9090/api/books
Content-Type: application/json
{
  "title": "",
  "author": "",
  "price": 0
}

-- PUT http://localhost:9090/api/books/{{id}}: Updates a specific book by ID.
Content-Type: application/json
{
  "id": 0,
  "title": "",
  "author": "",
  "price": 0
}

-- DELETE http://localhost:9090/api/books/{{id}}: Deletes a specific book by ID.

These endpoints collectively provide CRUD (Create, Read, Update, Delete) operations for managing books in a RESTful manner over HTTP. Each endpoint corresponds to a specific operation on the book entity, facilitating interaction with the server’s book data through standard HTTP methods and JSON payloads.

3.9 Unit Testing

Finally, let’s write some unit tests for our controller:

@WebMvcTest(BookController.class)
public class BookControllerTest {
    @Autowired
    private MockMvc mockMvc;
    @MockBean
    private BookRepository bookRepository;
    @Autowired
    private ObjectMapper objectMapper;

    @Test
    public void shouldReturnAllBooks() throws Exception {
        Book book = new Book();
        book.setId(1L);
        book.setTitle("Test Title");
        book.setAuthor("Test Author");
        book.setPrice(19.99);

        given(bookRepository.findAll()).willReturn(List.of(book));
        mockMvc.perform(get("/api/books"))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$[0].title").value(book.getTitle()));
    }

    @Test
    public void shouldReturnBookById() throws Exception {
        Book book = new Book();
        book.setId(1L);
        book.setTitle("Test Title");
        book.setAuthor("Test Author");
        book.setPrice(19.99);

        given(bookRepository.findById(book.getId())).willReturn(Optional.of(book));
        mockMvc.perform(get("/api/books/{id}", book.getId()))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.title").value(book.getTitle()));
    }

    @Test
    public void shouldCreateBook() throws Exception {
        Book book = new Book();
        book.setTitle("Test Title");
        book.setAuthor("Test Author");
        book.setPrice(19.99);

        given(bookRepository.save(book)).willReturn(book);
        mockMvc.perform(post("/api/books")
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(objectMapper.writeValueAsString(book)))
                .andExpect(status().isOk());
    }

    @Test
    public void shouldUpdateBook() throws Exception {
        Book book = new Book();
        book.setId(1L);
        book.setTitle("Test Title");
        book.setAuthor("Test Author");
        book.setPrice(19.99);

        Book updatedBook = new Book();
        updatedBook.setTitle("Updated Title");
        updatedBook.setAuthor("Updated Author");
        updatedBook.setPrice(29.99);

        given(bookRepository.findById(book.getId())).willReturn(Optional.of(book));
        given(bookRepository.save(book)).willReturn(updatedBook);
        mockMvc.perform(put("/api/books/{id}", book.getId())
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(objectMapper.writeValueAsString(updatedBook)))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.title").value(updatedBook.getTitle()));
    }

    @Test
    public void shouldDeleteBook() throws Exception {
        Book book = new Book();
        book.setId(1L);

        given(bookRepository.findById(book.getId())).willReturn(Optional.of(book));
        mockMvc.perform(delete("/api/books/{id}", book.getId()))
                .andExpect(status().isNoContent());
        verify(bookRepository).delete(book);
    }
}

The test case defines:

  • @WebMvcTest(BookController.class): Specifies that the BookControllerTest class is a Spring MVC test for the BookController class, focusing on testing the web layer of the application without loading the full Spring context.
  • @Autowired private MockMvc mockMvc: Autowires (injects) a MockMvc instance, which is used to perform HTTP requests against the controller and verify responses.
  • @MockBean private BookRepository bookRepository: Mocks (creates a mock instance of) the BookRepository interface, allowing controlled interactions during testing without accessing a real database.
  • @Autowired private ObjectMapper objectMapper: Autowires (injects) an ObjectMapper instance, used for converting Java objects to JSON and vice versa during request and response handling.
  • @Test public void shouldReturnAllBooks(): Test method to verify the behavior of retrieving all books from the API.
    • Creates a sample Book object and sets its attributes.
    • Mocks the behavior of bookRepository.findAll() to return a list containing the sample book.
    • Performs an HTTP GET request to /api/books using mockMvc.perform(get("/api/books")).
    • Verifies that the HTTP status returned is isOk() (200) and checks that the response JSON contains the expected title of the book using jsonPath("$[0].title").value(book.getTitle()).
  • Other test methods (shouldReturnBookById, shouldCreateBook, shouldUpdateBook, shouldDeleteBook): Each test method follows a similar structure:
    • Creates necessary Book objects and sets their attributes.
    • Mocks appropriate bookRepository methods (findById, save, delete) to simulate database interactions.
    • Performs HTTP requests (GET, POST, PUT, DELETE) to specific endpoints using mockMvc.perform().
    • Verifies the expected HTTP status and validates response content using assertions like jsonPath to ensure the correctness of data returned by the API.

4. Conclusion

Creating a REST API controller using the RestController annotation in a Spring Boot application is a streamlined and efficient way to develop robust and scalable web services. Spring Boot’s integration with Spring MVC simplifies the configuration and development process, allowing developers to focus on implementing business logic rather than boilerplate code. With a clear understanding of how to set up your project, define endpoints, and handle requests, you can leverage the full power of Spring Boot to build efficient and maintainable RESTful APIs. Whether you are building a small service or a complex system, mastering these skills is essential for modern web development.

5. Download the source code

In this tutorial, we demonstrated how to use RestController annotation to perform CRUD operations via the JPA interface.

Download
You can download the full source code of this example here: Spring Boot MVC REST Controller Example & Unit Tests

Yatin Batra

An experience full-stack engineer well versed with Core Java, Spring/Springboot, MVC, Security, AOP, Frontend (Angular & React), and cloud technologies (such as AWS, GCP, Jenkins, Docker, K8).
Subscribe
Notify of
guest

This site uses Akismet to reduce spam. Learn how your comment data is processed.

0 Comments
Inline Feedbacks
View all comments
Back to top button