Build RESTFul API with Spring Boot Build RESTFul API with Spring Boot

Page content

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:-

https://start.spring.io/#!type=maven-project&language=java&platformVersion=2.5.0.RELEASE&packaging=jar&jvmVersion=11&groupId=com.example&artifactId=demo&name=demo&description=Demo%20project%20for%20Spring%20Boot&packageName=com.example.demo&dependencies=web

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:-

https://start.spring.io/#!type=gradle-project&language=java&platformVersion=2.5.0.RELEASE&packaging=jar&jvmVersion=11&groupId=com.example&artifactId=demo&name=demo&description=Demo%20project%20for%20Spring%20Boot&packageName=com.example.demo&dependencies=web

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:-

  1. You have spring-boot-starter-web dependency in your pom.xml or build.gradle
  2. 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:-

  1. Use @RestController at class level, it helps to bind default HTTP Converters for you, e.g. when you return a User object from controller methods, it takes care of converting them in JSON.
  2. Use @RequestMapping at class level, to map APIs to URL.
  3. Use shorthand of @RequestMapping i.e. @GetMapping, @PostMapping, @PutMapping, @DeleteMapping at method level.
  4. Use @ResponseStatus at method level for appropriate HTTP Stats Code.
  5. 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:-

  1. Use @Service at class level, which auto register service class bean to Spring boot context.
  2. 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

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:-

  1. 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.
  2. We have autowired MockMvc and mocked UserService class in this example.
  3. We have used @AutoConfigureMockMvc annotation at class level which auto configure MockMvc 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:-

  1. We first mock the behavior of service layer using Mockito methods
  2. then we perform HTTP operation using MockMvc which delegate the request to mocked service layer instead of actual one.
  3. 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:-

  1. We first mock the behavior of client layer using Mockito methods
  2. then we call method of service layer which delegates the call to mocked client layer instead of actual one.
  3. 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