Build RESTFul API with Spring Boot
In this article, we’ll learn to build RESTFul APIs in Spring Boot Web Application using layered architecture and test driven development approach.
Project Setup
For initial setup of your Spring Boot project, you should use Spring Initializr. Choose the Spring Web dependency.
Maven Project
You can click the below link to generate a maven project with pre-selected dependencies:-
A typical pom.xml file for a web project look like this:-
<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.5.0</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.example</groupId>
<artifactId>api</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>api</name>
<description>Create Feign Client to consume RESTFul APIs</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</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>
Gradle Project
Similarly, You can click the below link to generate a Gradle project with pre-selected dependencies:-
A typical build.gradle file for a web project look like this:-
plugins {
id 'org.springframework.boot' version '2.5.0'
id 'io.spring.dependency-management' version '1.0.11.RELEASE'
id 'java'
}
group = 'com.example'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '11'
repositories {
mavenCentral()
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
}
test {
useJUnitPlatform()
}
Enable Web MVC
When you are working with spring boot project, you have nothing much to do to enable spring web mvc for your project. Make sure:-
- You have
spring-boot-starter-web
dependency in your pom.xml or build.gradle - You are using
@SpringBootApplication
on your application starter class file.
Spring Boot is opinionated, when it sees the web dependency in the classpath, it sets up all the necessary default configuration required for API development so that you can just concentrate on your business logic.
package com.example.api;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class ApiApplication {
public static void main(String[] args) {
SpringApplication.run(ApiApplication.class, args);
}
}
Build RESTFul APIs
We use the layered architecture to build RESTFul APIs where Controller Layer is the front liners to serve API request and delegate the request to Service Layer and then to Repository or Client layer and so and so forth. Once the requested data is available, it is returned back in the same order for e.g. Repository or Client to Service to Controller layer.
Controller Layer
We are creating a UserController
class to build RESTFul APIs for CRUD operations. Few things to understand:-
- Use
@RestController
at class level, it helps to bind default HTTP Converters for you, e.g. when you return aUser
object from controller methods, it takes care of converting them in JSON. - Use
@RequestMapping
at class level, to map APIs to URL. - Use shorthand of
@RequestMapping
i.e.@GetMapping
,@PostMapping
,@PutMapping
,@DeleteMapping
at method level. - Use
@ResponseStatus
at method level for appropriate HTTP Stats Code. - Delegate the work to Service Layer, here using
UserService
package com.example.api.controller;
@RestController
@RequestMapping("/users")
public class UserController {
@Autowired
private UserService userService;
@GetMapping
public Users getAllUsers() {
return userService.getAllUsers();
}
@GetMapping("/{id}")
public User getUserById(@PathVariable Long id) {
return userService.getUserById(id);
}
@PostMapping
@ResponseStatus(HttpStatus.CREATED)
public Long createUser(User user) {
return userService.createUser(user);
}
@PutMapping("/{id}")
@ResponseStatus(HttpStatus.OK)
public void updateUser(@PathVariable Long id, User user) {
userService.updateUser(id, user);
}
@DeleteMapping("/{id}")
@ResponseStatus(HttpStatus.OK)
public void deleteUserById(@PathVariable Long id) {
userService.deleteUserById(id);
}
}
Service Layer
We are creating a UserService
class which takes care of business logic. Few things to understand:-
- Use
@Service
at class level, which auto register service class bean to Spring boot context. - Service class can further fetch the data from:-
- Repository classes (used to fetch data from Database by extending Spring Boot
JpaRepository
) - Client classes (used to fetch data from thirdparty APIs using FeignClient or
RestTemplate
) - Any other data provider
- Repository classes (used to fetch data from Database by extending Spring Boot
In This example, our UserService
is using UserMockClient
to fetch mock data.
package com.example.api.service;
@Service
public class UserServiceImpl implements UserService {
@Autowired
private UserMockClient userMockClient;
@Override
public List<User> getAllUsers() {
return userMockClient.getAllUsers();
}
@Override
public User getUserById(Long id) {
return userMockClient.getUserById(id);
}
@Override
public Long createUser(User user) {
return userMockClient.createUser(user);
}
@Override
public boolean updateUser(Long id, User user) {
return userMockClient.updateUser(id, user);
}
@Override
public boolean deleteUserById(Long id) {
return userMockClient.deleteUserById(id);
}
}
That’s it. Just start your application and test your APIs.
Unit test for Controller and Service classes
It is a good practice to write unit test cases for Controller and Service classes you created.
Unit Test for Controller Class
Let’s create a class UserControllerTest
in src/test/java
directory and follow the same package structure as our UserController
class. Things to note that:-
- We have used
@SpringBootTest
annotation at class level which configure a spring boot context of your application before executing test cases. This gives us ability to bind actual spring bean using@Autowired
and mock the spring bean using@MockBean
annotation. - We have autowired
MockMvc
and mockedUserService
class in this example. - We have used
@AutoConfigureMockMvc
annotation at class level which auto configureMockMvc
which is used to perform HTTP operations using Mockito framework.
Mockito framework is used to mock the behavior of classes. In this example, we mocked the service layer and HTTP operations.
In each test case of controller layer:-
- We first mock the behavior of service layer using Mockito methods
- then we perform HTTP operation using
MockMvc
which delegate the request to mocked service layer instead of actual one. - then we assert with expected response
package com.example.api.controller;
@SpringBootTest
@AutoConfigureMockMvc
public class UserControllerTest {
@Autowired
private MockMvc mockMvc;
@MockBean
private UserService userService;
@Test
public void getAllUsers_whenValidRequest_returnsValidResponse() throws Exception {
// mock service class behavior
when(userService.getAllUsers()).thenReturn(UserTestData.users());
// perform HTTP operation using MockMvc
mockMvc.perform(get("/users"))
.andExpect(status().is2xxSuccessful())
.andExpect(jsonPath("$", hasSize(UserTestData.users().size())))
.andExpect(jsonPath("$[0].id", equalTo(1)))
.andExpect(jsonPath("$[0].name", equalTo("Adam")))
.andExpect(jsonPath("$[0].dateOfBirth", equalTo("22 Aug 1986")));
}
@Test
public void getAllUsers_whenServiceThrowException_returnsInternalServerError() throws Exception {
when(userService.getAllUsers()).thenThrow(new RuntimeException("Oops, Something went wrong!"));
mockMvc.perform(get("/users"))
.andExpect(status().isInternalServerError())
.andExpect(jsonPath("$.message", equalTo("Internal Server Error")))
.andExpect(jsonPath("$.debugMessage", equalTo("Oops, Something went wrong!")));
}
@Test
public void getUserById_whenValidUserId_returnThatUser() throws Exception {
when(userService.getUserById(anyLong())).thenReturn(UserTestData.user());
mockMvc.perform(get("/users/1"))
.andExpect(status().is2xxSuccessful())
.andExpect(jsonPath("$.id", equalTo(1)))
.andExpect(jsonPath("$.name", equalTo("Adam")))
.andExpect(jsonPath("$.dateOfBirth", equalTo("22 Aug 1986")));
}
@Test
public void createUser_whenValidUserData_createAndReturnTheUserId() throws Exception {
when(userService.createUser(UserTestData.user())).thenReturn(UserTestData.user().getId());
mockMvc.perform(post("/users")
.contentType(MediaType.APPLICATION_JSON)
.content(asJsonString(UserTestData.user())))
.andExpect(status().is2xxSuccessful());
}
@Test
public void updateUser_whenValidUserId_updateThatUser() throws Exception {
mockMvc.perform(put("/users/1"))
.andExpect(status().is2xxSuccessful());
}
@Test
public void deleteUser_whenValidUserId_deleteThatUser() throws Exception {
mockMvc.perform(delete("/users/1"))
.andExpect(status().is2xxSuccessful());
}
@Test
public void patchUser_whenUnsupportedHttpVerb_returnsMethodNotAllowed() throws Exception {
mockMvc.perform(patch("/users"))
.andExpect(status().isMethodNotAllowed())
.andExpect(jsonPath("$.message", equalTo("Method Not Allowed")))
.andExpect(jsonPath("$.debugMessage", equalTo("Request method 'PATCH' not supported")));
}
public static String asJsonString(final Object obj) {
try {
return new ObjectMapper().writeValueAsString(obj);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
Unit Test for Service Class
Let’s create a class UserServiceTest
in src/test/java
directory and follow the same package structure as our UserService
class.
Here we again used @SpringBootTest
annotation to bind the actual spring bean of UserService
and the mocked spring bean of client layer UserMockClient
.
In each test case of service layer:-
- We first mock the behavior of client layer using Mockito methods
- then we call method of service layer which delegates the call to mocked client layer instead of actual one.
- then we assert with expected output
package com.example.api.service;
@SpringBootTest
public class UserServiceTest {
@MockBean
private UserMockClient userMockClient;
@Autowired
private UserService userService;
@Test
public void getAllUsers_whenValidProviderResponse_returnAllUsers() {
when(userMockClient.getAllUsers()).thenReturn(UserTestData.users());
List<User> users = userService.getAllUsers();
assertThat(users.size()).isEqualTo(1);
assertThat(users.get(0).getId()).isEqualTo(1);
assertThat(users.get(0).getName()).isEqualTo("Adam");
assertThat(users.get(0).getDateOfBirth().toString()).isEqualTo("1986-08-22");
}
@Test
public void getUserById_whenValidUserId_returnThatUser() {
when(userMockClient.getUserById(anyLong())).thenReturn(UserTestData.user());
User user = userService.getUserById(1L);
assertThat(user.getId()).isEqualTo(1);
assertThat(user.getName()).isEqualTo("Adam");
assertThat(user.getDateOfBirth().toString()).isEqualTo("1986-08-22");
}
@Test
public void createUser_whenValidUserData_createAndReturnUserId() {
when(userMockClient.createUser(any(User.class))).thenReturn(UserTestData.user().getId());
Long id = userService.createUser(UserTestData.user());
assertThat(id).isEqualTo(1L);
}
@Test
public void updateUser_whenValidUserData_updateAndReturnStatus() {
when(userMockClient.updateUser(anyLong(), any(User.class))).thenReturn(true);
Boolean status = userService.updateUser(UserTestData.user().getId(), UserTestData.user());
assertThat(status).isTrue();
}
@Test
public void deleteUser_whenValidUserId_deleteAndReturnStatus() {
when(userMockClient.deleteUserById(anyLong())).thenReturn(true);
Boolean status = userService.deleteUserById(UserTestData.user().getId());
assertThat(status).isTrue();
}
}
Download the complete source code for this example from github/springboot-api