Userbook — Complete microservice using Springboot 3.

Hemant
5 min readSep 5, 2023

What is Userbook Application?

Userbook Application provide CRUD (Create, Read, Update & Delete) operations on user’s data.

What are we building?

We are building REST APIs for backend service of Userbook Application.

Which APIs ?

We will write CRUD operation APIs

  1. GET /users — return all users (paginated)
  2. GET /users/{id} — returns user by id
  3. POST /users — creates new user
  4. PUT /users/{id} — update existing user
  5. DELETE /users/{id} — deletes user by Id

Which tech stack is used?

Java 17, Spring boot 3, H2 Database, Maven

How project is generated?

Project is generated using Spring Initializr. But it can also be generated using intelliJ Idea or STS.

  • You may find difference in versions of dependencies but code will work with Spring boot 3.x and Java 17.

What is Code Structure?

code structure

User Controller

package com.hk.prj.userbook.user;

import com.hk.prj.userbook.exception.InvalidMethodException;
import jakarta.validation.Valid;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Sort;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.support.ServletUriComponentsBuilder;

import java.net.URI;
import java.util.List;
import java.util.Objects;

@RestController
public class UserController {

private final UserService userService;

@Autowired
UserController(UserService userService) {
this.userService = userService;
}

@GetMapping("/users")
public List<User> getUsers(@RequestParam("offset") int offset,
@RequestParam("page_size") int pageSize,
@RequestParam("sort_by") String sortBy) {
return userService.getUsers(PageRequest.of(offset, pageSize, Sort.by(sortBy)));
}

@GetMapping("/users/{id}")
public User getUserById(@PathVariable Long id) {
return userService.getUserById(id);
}

@PostMapping("/users")
public ResponseEntity<User> saveUser(@Valid @RequestBody User user) {
if (Objects.nonNull(user.getId()))
throw new InvalidMethodException("id is present, Use PUT instead of POST");
User savedUser = userService.saveUser(user);
URI uri = ServletUriComponentsBuilder.fromCurrentRequestUri()
.path("/{id}")
.buildAndExpand(savedUser.getId()).toUri();
return ResponseEntity.created(uri).build();
}

@DeleteMapping("/users/{id}")
public void deleteUser(@PathVariable Long id) {
userService.deleteUser(id);
}
}

User Service

package com.hk.prj.userbook.user;

import com.hk.prj.userbook.exception.UserNotFoundException;
import org.springframework.data.domain.PageRequest;
import org.springframework.stereotype.Service;

import java.util.List;

@Service
public class UserService {
private final UserRepository userRepository;

public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}

public List<User> getUsers(PageRequest pageRequest) {
return userRepository.findAll(pageRequest).stream().toList();
}

public User getUserById(Long id) {
return userRepository.findById(id)
.orElseThrow(() -> new UserNotFoundException("User not found with id " + id));
}

public User saveUser(User user) {
return userRepository.save(user);
}

public void deleteUser(long userId) {
User user = getUserById(userId);
userRepository.delete(user);
}
}

User Repository

package com.hk.prj.userbook.user;

import org.springframework.data.jpa.repository.JpaRepository;

public interface UserRepository extends JpaRepository<User, Long> {
}

Database

H2 Database with initial schema and data using .sql files in resources folder.

Testing

Tests are written using Mockito bundled with spring-boot-starter-test.

Controller Tests

package com.hk.prj.userbook;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.hk.prj.userbook.exception.UserNotFoundException;
import com.hk.prj.userbook.user.User;
import com.hk.prj.userbook.user.UserController;
import com.hk.prj.userbook.user.UserService;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Sort;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;

import static org.hamcrest.Matchers.containsString;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.when;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;

/**
* WebMvcTest = SpringBootTest + AutoConfigureMockMvc
*/
@WebMvcTest(controllers = UserController.class, excludeAutoConfiguration = SecurityAutoConfiguration.class)
public class UserControllerTests {

@Autowired
private MockMvc mockMvc;

@MockBean
private UserService userService;

@Test
@DisplayName("Get all users")
void getAllUsers_success() throws Exception {
when(userService.getUsers(PageRequest.of(1, 20, Sort.by("email")))).thenReturn(UserUtil.getUsers());
mockMvc.perform(get("/users?offset=1&page_size=20&sort_by=email"))
.andExpect(status().is2xxSuccessful())
.andExpect(content().json(asJsonString(UserUtil.getUsers())));
}

@Test
@DisplayName("Get users by Id - found")
void getUsersById_success() throws Exception {
when(userService.getUserById(1L)).thenReturn(UserUtil.getUsers().get(0));
mockMvc.perform(get("/users/1"))
.andExpect(status().is2xxSuccessful())
.andExpect(content().json(asJsonString(UserUtil.getUsers().get(0))));
}

@Test
@DisplayName("Get users by Id - Not found")
void getUsersById_failed() throws Exception {
when(userService.getUserById(3L)).thenThrow(UserNotFoundException.class);
mockMvc.perform(get("/users/3"))
.andExpect(status().isNotFound());
}

@Test
@DisplayName("Save user success")
void saveUsers_success() throws Exception {
when(userService.saveUser(any(User.class))).thenReturn(UserUtil.getUsers().get(0));
mockMvc.perform(post("/users")
.contentType(MediaType.APPLICATION_JSON)
.content(asJsonString(UserUtil.getUsers().get(0))))
.andExpect(status().isCreated())
.andExpect(header().exists("Location"));
}

@Test
@DisplayName("Save blank first name user - bad request")
void saveBlankFirstNameUsers_returnBadRequest() throws Exception {
User user = UserUtil.getUsers().get(0);
user.setFirstName("");
mockMvc.perform(post("/users")
.contentType(MediaType.APPLICATION_JSON)
.content(asJsonString(user)))
.andExpect(status().isBadRequest())
.andExpect(jsonPath("$.errorCode", containsString("first name should have atleast 2 characters")));
}

@Test
@DisplayName("Save first name 1 letter user - bad request")
void saveFirstName1LetterUser_returnBadRequest() throws Exception {
User user = UserUtil.getUsers().get(0);
user.setFirstName("H");
mockMvc.perform(post("/users")
.contentType(MediaType.APPLICATION_JSON)
.content(asJsonString(user)))
.andExpect(status().isBadRequest())
.andExpect(jsonPath("$.errorCode", containsString("first name should have atleast 2 characters")));
}

@Test
@DisplayName("Save blank Last name user - bad request")
void saveBlankLastNameUsers_returnBadRequest() throws Exception {
User user = UserUtil.getUsers().get(0);
user.setLastName("");
mockMvc.perform(post("/users")
.contentType(MediaType.APPLICATION_JSON)
.content(asJsonString(user)))
.andExpect(status().isBadRequest())
.andExpect(jsonPath("$.errorCode").value("last name can't be empty"));
}

@Test
@DisplayName("Save blank name user - bad request")
void saveBlankNameUsers_returnBadRequest() throws Exception {
User user = UserUtil.getUsers().get(0);
user.setFirstName("");
user.setLastName("");
mockMvc.perform(post("/users")
.contentType(MediaType.APPLICATION_JSON)
.content(asJsonString(user)))
.andExpect(status().isBadRequest())
.andExpect(jsonPath("$.errorCode", containsString("first name should have atleast 2 characters")));
}

@Test
@DisplayName("Post user with Id - bad request and message")
void saveUsersWithId_returnBadRequestWithMessage() throws Exception {
User user = UserUtil.getUsers().get(0);
user.setId(1L);
mockMvc.perform(post("/users")
.contentType(MediaType.APPLICATION_JSON)
.content(asJsonString(user)))
.andExpect(status().isBadRequest())
.andExpect(jsonPath("$.errorDescription").value("id is present, Use PUT instead of POST"));
}

@Test
@DisplayName("Delete users by Id - Not found")
void deleteUsersById_failed() throws Exception {
doThrow(UserNotFoundException.class).when(userService).deleteUser(3L);
mockMvc.perform(delete("/users/3"))
.andExpect(status().isNotFound());
}

@Test
@DisplayName("Delete users by null Id - get exception")
void deleteUsersByNullId_failed() throws Exception {
mockMvc.perform(delete("/users/null"))
.andExpect(status().isBadRequest());
}

@Test
@DisplayName("Delete users by Id - found")
void deleteUsersById_success() throws Exception {
mockMvc.perform(delete("/users/1"))
.andExpect(status().isOk());
}

public static String asJsonString(final Object obj) {
try {
return new ObjectMapper().writeValueAsString(obj);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}

Service Tests

package com.hk.prj.userbook;


import com.hk.prj.userbook.user.User;
import com.hk.prj.userbook.exception.UserNotFoundException;
import com.hk.prj.userbook.user.UserRepository;
import com.hk.prj.userbook.user.UserService;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Sort;

import java.util.Optional;

import static org.assertj.core.api.AssertionsForClassTypes.assertThat;
import static org.mockito.Mockito.when;

@SpringBootTest(classes = UserService.class)
public class UserServiceTests {

@Autowired
UserService userService;

@MockBean
UserRepository userRepository;


@Test
public void getAllUsers_Test() {
when(userRepository.findAll(PageRequest.of(1, 20, Sort.by("email")))).thenReturn(UserUtil.getUsersPage());
assertThat(userService.getUsers(PageRequest.of(1, 20, Sort.by("email"))).size()).isNotEqualTo(0);
}

@Test
public void getUsersById_success() {
when(userRepository.findById(1L)).thenReturn(Optional.of(UserUtil.getUsers().get(0)));
User user = userService.getUserById(1L);
assertThat(user.getFirstName()).isEqualTo("H1");
assertThat(user.getLastName()).isEqualTo("K1");
}

@Test
public void getUsersById_failed() {
when(userRepository.findById(2L)).thenThrow(new UserNotFoundException("User not found with id 2"));
try {
userService.getUserById(2L);
} catch (UserNotFoundException u) {
assertThat(u).isNotNull();
assertThat(u.getMessage()).isEqualTo("User not found with id 2");
}
}

@Test
public void deleteUsersById_failed() {
when(userRepository.findById(2L)).thenThrow(new UserNotFoundException("User not found with id 2"));
try {
userService.deleteUser(2L);
} catch (UserNotFoundException u) {
assertThat(u).isNotNull();
assertThat(u.getMessage()).isEqualTo("User not found with id 2");
}
}
}

Repository Tests

package com.hk.prj.userbook;

import com.hk.prj.userbook.user.User;
import com.hk.prj.userbook.user.UserRepository;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;

import java.util.List;

import static org.assertj.core.api.AssertionsForClassTypes.assertThat;

@DataJpaTest
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.AUTO_CONFIGURED)
public class UserRepositoryTest {

@Autowired
UserRepository userRepository;

@Test
@DisplayName("Autowired User Repository")
public void testAutowiredUserRepository(){
assertThat(userRepository).isNotNull();
}

@Test
@DisplayName("find all")
public void initial_data_count_success(){
assertThat(userRepository.findAll().size()).isEqualTo(25);
}

@Test
@DisplayName("save user")
public void testSaveUser_success(){
User savedUser = userRepository.save(UserUtil.getUsers().get(0));
assertThat(savedUser).isNotNull();
assertThat(savedUser.getFirstName()).isEqualTo("H1");
assertThat(savedUser.getLastName()).isEqualTo("K1");
assertThat(savedUser.getId()).isNotNull();
}

@Test
@DisplayName("save users list")
public void testSaveUsers_success(){
List<User> savedUsers = userRepository.saveAll(UserUtil.getUsers());
assertThat(savedUsers.size()).isEqualTo(3);
assertThat(savedUsers.get(0).getFirstName()).isEqualTo("H1");
assertThat(savedUsers.get(0).getLastName()).isEqualTo("K1");
assertThat(savedUsers.get(0).getId()).isNotNull();
}

@Test
@DisplayName("delete users success")
public void deleteUsers_success(){
List<User> savedUsers = userRepository.findAll();
assertThat(savedUsers.size()).isEqualTo(25);
userRepository.delete(savedUsers.get(0));
assertThat(userRepository.findAll().size()).isEqualTo(24);
}

}

Code repo link

How to run?

  1. Clone code from git link above.
  2. Compile and install dependencies using command -

mvn clean install

  • Above command will also run test cases.

3. Start Application using command -

mvn spring-boot:run

4. Application is available at http://localhost:8080, verify using -

http://localhost:8080/users?offset=0&page_size=10&sort_by=id

Happy Coding

--

--

Hemant

Software Engineering @JPMorgan & Chase. Writes about Java, Spring boot, Microservices, AWS and System Design