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.
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;
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
BookControllerclass 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
BookRepositoryinto 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().
- Returns a list of all books by calling
- @GetMapping(“/{id}”): Handles HTTP GET requests to retrieve a book by its ID.
- Uses
@PathVariable Long idto 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, orResponseEntity.notFound().build()if not.
- Uses
- @PostMapping: Handles HTTP POST requests to create a new book.
- Validates the incoming request body (
@Valid @RequestBody Book book) against validation rules defined in theBookclass. - Saves the new book to the database using
bookRepository.save(book). - Returns the saved book object.
- Validates the incoming request body (
- @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, orResponseEntity.notFound().build()if the book does not exist.
- Similar to
- @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, orResponseEntity.notFound().build()if the book does not exist.
- Attempts to find the book by ID and deletes it using
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
GlobalExceptionHandlerclass is a global exception handler for the entire Spring application, providing centralized error handling for all controllers. - @ExceptionHandler(MethodArgumentNotValidException.class): Specifies that the
handleValidationExceptionsmethod will handle exceptions of typeMethodArgumentNotValidException. - handleValidationExceptions(MethodArgumentNotValidException ex): Method that handles validation exceptions thrown during request processing.
- Creates a new
HashMapcallederrorsto store field errors. - Uses
ex.getBindingResult().getFieldErrors()to iterate through field errors and populate theerrorsmap with field names as keys and error messages as values. - Returns a
ResponseEntitycontaining theerrorsmap and HTTP statusHttpStatus.BAD_REQUEST(400), indicating that the request was malformed or contained invalid data.
- Creates a new
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
BookControllerTestclass is a Spring MVC test for theBookControllerclass, focusing on testing the web layer of the application without loading the full Spring context. - @Autowired private MockMvc mockMvc: Autowires (injects) a
MockMvcinstance, which is used to perform HTTP requests against the controller and verify responses. - @MockBean private BookRepository bookRepository: Mocks (creates a mock instance of) the
BookRepositoryinterface, allowing controlled interactions during testing without accessing a real database. - @Autowired private ObjectMapper objectMapper: Autowires (injects) an
ObjectMapperinstance, 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
Bookobject 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/booksusingmockMvc.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 usingjsonPath("$[0].title").value(book.getTitle()).
- Creates a sample
- Other test methods (
shouldReturnBookById,shouldCreateBook,shouldUpdateBook,shouldDeleteBook): Each test method follows a similar structure:- Creates necessary
Bookobjects and sets their attributes. - Mocks appropriate
bookRepositorymethods (findById,save,delete) to simulate database interactions. - Performs HTTP requests (
GET,POST,PUT,DELETE) to specific endpoints usingmockMvc.perform(). - Verifies the expected HTTP status and validates response content using assertions like
jsonPathto ensure the correctness of data returned by the API.
- Creates necessary
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.
You can download the full source code of this example here: Spring Boot MVC REST Controller Example & Unit Tests






