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
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()
.
- Returns a list of all books by calling
- @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, 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 theBook
class. - 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
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 typeMethodArgumentNotValidException
. - handleValidationExceptions(MethodArgumentNotValidException ex): Method that handles validation exceptions thrown during request processing.
- Creates a new
HashMap
callederrors
to store field errors. - Uses
ex.getBindingResult().getFieldErrors()
to iterate through field errors and populate theerrors
map with field names as keys and error messages as values. - Returns a
ResponseEntity
containing theerrors
map 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
BookControllerTest
class is a Spring MVC test for theBookController
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
usingmockMvc.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
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 usingmockMvc.perform()
. - Verifies the expected HTTP status and validates response content using assertions like
jsonPath
to 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