Published on

Part 3: Let’s build a simple spring boot REST API - Set up a service layer

Authors

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.