- Published on
Part 3: Let’s build a simple spring boot REST API - Set up a service layer
- Authors
- Name
- Ryuichi Nishi
- @ryuichi2c
In the previous chapter, we set up a controller and made 5 endpoints to do CRUD operations. The API works very fine, but if you consider the maintainability and extendability of the code base, writing all business logic in controllers is a bad practice because it violates the responsibility of a controller and the controller has too many other responsibilities. This is why we will introduce a service layer to this project to remove all business logic from the controller and put them into the service layer.
Previous Chapter
https://www.juniordevmind.com/blog/part-2-Lets-build-a-simple-spring-boot-REST-API
Source code for this article
https://github.com/ryuichi24/simple-spring-rest-api/tree/4-setup-service-layer
Project structure
simplespringrestapi
├── SimpleSpringRestApiApplication.java
├── controllers
│ └── TodoController.java # <- updated!
├── models
│ └── TodoItem.java
└── services # <- new!
├── TodoService.java
└── TodoServiceImpl.java
Code service interface
TodoService.java
package tech.ryuichi24.simplespringrestapi.services;
import java.util.List;
import tech.ryuichi24.simplespringrestapi.models.TodoItem;
public interface TodoService {
public TodoItem saveTodoItem(TodoItem todoItem);
public List<TodoItem> getTodoItems();
public TodoItem getTodoItemById(int id);
public void removeTodoItemById(int id);
public TodoItem updateTodoItem(int id, TodoItem todoItem);
}
simplespringrestapi
├── SimpleSpringRestApiApplication.java
├── controllers
│ └── TodoController.java
├── models
│ └── TodoItem.java
└── services
├── TodoService.java # <- this one!
└── TodoServiceImpl.java
Firstly, I defined TodoService
as an interface that TodoServiceImpl
will implements. The reason why I defined the interface is that the controller, which is the user of the TodoService
, should not depend on the implementation of the service but on the abstraction or interface so that the implementation can easily swapped with the other one such as a mock service class for testing purposes. So you should not instantiate TodoServiceImpl
class within the TodoController
class. Instead, the controller gets the service as an interface and lets Spring Boot instantiate and inject the implementation as a dependency.
Code service implementation
TodoServiceImpl.java
package tech.ryuichi24.simplespringrestapi.services;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.atomic.AtomicInteger;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Service;
import org.springframework.web.server.ResponseStatusException;
import tech.ryuichi24.simplespringrestapi.models.TodoItem;
@Service
public class TodoServiceImpl implements TodoService {
private final AtomicInteger _counter = new AtomicInteger();
private final List<TodoItem> _todoItems = new ArrayList<>() {
{
add(new TodoItem(_counter.incrementAndGet(), "todo 1"));
add(new TodoItem(_counter.incrementAndGet(), "todo 2"));
add(new TodoItem(_counter.incrementAndGet(), "todo 3"));
}
};
@Override
public TodoItem saveTodoItem(TodoItem todoItem) {
if (Objects.isNull(todoItem.getTitle())) {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Title must not be null.");
}
todoItem.setId(_counter.incrementAndGet());
_todoItems.add(todoItem);
return todoItem;
}
@Override
public List<TodoItem> getTodoItems() {
return this._todoItems;
}
@Override
public TodoItem getTodoItemById(int id) {
TodoItem found = _findTodoItemById(id);
return found;
}
@Override
public void removeTodoItemById(int id) {
TodoItem found = _findTodoItemById(id);
_todoItems.remove(found);
}
@Override
public TodoItem updateTodoItem(int id, TodoItem todoItem) {
TodoItem found = _findTodoItemById(id);
_todoItems.remove(found);
_todoItems.add(todoItem);
return todoItem;
}
private TodoItem _findTodoItemById(int id) {
Optional<TodoItem> found = _todoItems.stream().filter(item -> item.getId() == id).findAny();
if (found.isPresent()) {
throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Not Found");
}
return found.get();
}
}
simplespringrestapi
├── SimpleSpringRestApiApplication.java
├── controllers
│ └── TodoController.java
├── models
│ └── TodoItem.java
└── services
├── TodoService.java
└── TodoServiceImpl.java # <- this one!
When you define a service class, you must add one annotation to it, which is @Service
. This annotation lets Spring Boot know the existence of the class and register it in the dependency container so that it can be injected into the controller class.
Refactor controller class with service class
TodoController.java
package tech.ryuichi24.simplespringrestapi.controllers;
import java.net.URI;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.support.ServletUriComponentsBuilder;
import tech.ryuichi24.simplespringrestapi.models.TodoItem;
import tech.ryuichi24.simplespringrestapi.services.TodoService;
@RequestMapping(path = TodoController.BASE_URL)
@RestController
public class TodoController {
public static final String BASE_URL = "/api/v1/todos";
@Autowired
private TodoService _todoService;
@GetMapping(path = "")
public ResponseEntity<List<TodoItem>> getTodoItems() {
List<TodoItem> todoItems = _todoService.getTodoItems();
return ResponseEntity.ok(todoItems);
}
@GetMapping(path = "/{id}")
public ResponseEntity<TodoItem> getTodoItem(@PathVariable int id) {
TodoItem found = _todoService.getTodoItemById(id);
return ResponseEntity.ok(found);
}
@PostMapping(path = "")
public ResponseEntity<TodoItem> createTodoItem(@RequestBody TodoItem newTodoItem) {
TodoItem savedTodoItem = _todoService.saveTodoItem(newTodoItem);
URI location = ServletUriComponentsBuilder.fromCurrentRequest().path("/{id}")
.buildAndExpand(savedTodoItem.getId()).toUri();
return ResponseEntity.created(location).body(savedTodoItem);
}
@PutMapping(path = "/{id}")
public ResponseEntity<?> updateTodoItem(@PathVariable int id, @RequestBody TodoItem newTodoItem) {
_todoService.updateTodoItem(id, newTodoItem);
return ResponseEntity.noContent().build();
}
@DeleteMapping(path = "/{id}")
public ResponseEntity<?> removeTodoItem(@PathVariable int id) {
_todoService.removeTodoItemById(id);
return ResponseEntity.noContent().build();
}
}
simplespringrestapi
├── SimpleSpringRestApiApplication.java
├── controllers
│ └── TodoController.java # <- this one!
├── models
│ └── TodoItem.java
└── services
├── TodoService.java
└── TodoServiceImpl.java
TodoService
was added as its property and annotated with @Autowired
, which tells Spring Boot to inject a dependency registered in the container. In this case, TodoServiceImpl
gets injected.
Conclusion
We set up a service layer to decouple the business logic from the controller so that the controller can focus on its responsibility of controlling the data flow. As a result, the code base become much more clean and more maintainable and extendable.