DEV Community

Francisco Pereira
Francisco Pereira

Posted on

Creating a REST API using Spring Boot + Tests + Documentation [part 03]

Hi!

I'm back in this tutorial to teach you how to develop a REST API using Spring Boot. As I said earlier, this tutorial is divided into 5 parts as shown in the following table:

Part Content
Part 01 Application Startup
Part 02 Settings and Database
Part 03 API Implementation
Part 04 Tests + Coverage
Part 05 Swagger Documentation

It is expected that from this third part you will learn the following:

  • How to create a service containing the system's business rule
  • How to create the controllers containing the system's routes

Remember that in this tutorial the following tools are used:

  • JDK 17
  • IntelliJ
  • Maven
  • Insomnia
  • MySQL database

Step 01 - Creating the service

We created the Book class repository which will allow us to perform the necessary manipulations in the database (inserting, deleting, editing and viewing data, for example).

Now we can implement the system's business rule, this implementation can be done directly in Controller where we will have access routes to the system. But in doing so we are not following the Single Responsibility Principle. Therefore, Spring Boot indicates the creation of a service for the implementation of the business rule.

To create the service, we'll go into the src.main.java.com.simplelibrary.simplelibrary.API package and create a new package called service. And inside of this, we create an interface called BookService:

package com.simplelibrary.simplelibraryAPI.service;

import com.simplelibrary.simplelibraryAPI.dto.BookRequestDTO;
import com.simplelibrary.simplelibraryAPI.dto.BookResponseDTO;
import com.simplelibrary.simplelibraryAPI.model.Book;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;

import java.io.IOException;

public interface BookService {
    public Page<BookResponseDTO> getAllBooks(Pageable pagination);

    public BookResponseDTO show(Long id);

    public Book store(BookRequestDTO book) throws IOException;

    public BookResponseDTO update(Long id, BookRequestDTO book);

    public void delete(Long id);

    public long count();

}

Enter fullscreen mode Exit fullscreen mode
  • The getAllBooks method will be used to get all the books in the database. It will receive as a parameter an object of type Pageable which is responsible for pagination. Later when we implement the route, we'll talk about this class. Finally, it will return an object of type Page that will contain the contents of the BookResponseDTO class.

  • The show method will be used to get the information of only 1 book. It will receive the id of this book as a parameter and return the BookResponseDTO.

  • The store method will be used to store a book in the database. It will receive the information from the BookRequestDTO and return the Book. In its implementation we will talk more about this return.

  • The update method will be used to update the book information. It will receive the id of the book and its information (BookRequestDTO) and will return the BookResponseDTO.

  • The delete method will be used to delete a book from the database. For this we only need your id.

  • The count method will be used to check how many books have been registered in the database. A long value will be returned.

After creating the interface, we can now create its implementation. For that, we'll go into the src.main.java.com.simplelibrary.simplelibrary.API.service package and create a new package called impl. And inside this, we create an interface called BookServiceImpl.

Initially the class will have the following format:

import org.springframework.stereotype.Service;
import com.simplelibrary.simplelibraryAPI.service.BookService;
import org.springframework.beans.factory.annotation.Autowired;

@Service
public class BookServiceImpl implements BookService {

@Autowired
    private BookRepository bookRepository;

}

Enter fullscreen mode Exit fullscreen mode

As we are creating the implementation of a service, we need to insert the @Service annotation at the beginning of the class. By doing this, Spring will understand that this class is indeed a Service.

And notice the @Autowired annotation it is used for dependency injection. We want to inject the BookRepository to be used in your database manipulation methods. Therefore, we need to inform the @Autowired annotation before its declaration so that Spring can be taking care of this dependency.

Once this is done, we can implement the methods that belong to the interface we created earlier:

getAllBooks

@Override
public Page<BookResponseDTO> getAllBooks(Pageable pagination){
        var page = bookRepository.findAll(pagination).map(BookResponseDTO::new);
        return page;
    }

Enter fullscreen mode Exit fullscreen mode

The repository has a method called findAll which will return all the books present in the database. It can receive a Pageable object as a parameter that will contain the paging information (trust me, we'll talk more about how this object is built later).

This result, by default, will return objects of type Book (DAO), because in the declaration of the repository we inform in the generics that it is of type Book. However, we previously defined that the return to the user has to be of type BookResponseDTO. Therefore, we will make a map to call the BookResponseDTO constructor that will receive data from an object of type Book and fill in its attributes.

Finally, we will return the result that has been found.

show


  @Override
    public BookResponseDTO show(Long id) {
        return new BookResponseDTO(bookRepository.getReferenceById(id));
    }

Enter fullscreen mode Exit fullscreen mode

The show method is quite simple to understand.
We will use the getReferenceById method that will return a Book type object based on its id. Finally, we do the conversion to BookResponseDTO by calling its constructor.

store

@Override
    public Book store(BookRequestDTO bookRequest) throws IOException {
        var book = new Book(bookRequest);
        book.setRate(this.scrapRatings(book.getTitle()));
        bookRepository.save(book);
        return book;
    }

Enter fullscreen mode Exit fullscreen mode

The store method is used to store a book in the database. Notice that in the first line, a Book object is created and passed in its constructor the bookRequest object. Therefore, you will create the constructor inside the Book class:


 public Book(BookRequestDTO bookRequest){
        this.title = bookRequest.title();
        this.author = bookRequest.author();
        this.pages = bookRequest.pages();
        this.isbn = bookRequest.isbn();
        this.rate = bookRequest.rate();
        this.finished = bookRequest.finished();
    }

Enter fullscreen mode Exit fullscreen mode

Next, we make the logic for filling in the value of the book's rate. As presented in the system scenario, this attribute will be filled according to what is found on the site http://www.skoob.com.br.

To extract data from the web (web scraping) I am using the Jsoup library. You can go to your pom.xml file and import it:

<dependency>            
   <groupId>org.jsoup</groupId>
   <artifactId>jsoup</artifactId>
   <version>1.15.3</version>
</dependency>

Enter fullscreen mode Exit fullscreen mode

The scrapRatings method will look like this:


private Double scrapRatings(String title) {
        Double rating = Double.valueOf(0);
        try{
            String titleEdited = title.replace(" ","-");
            Document doc = Jsoup.connect("https://www.skoob.com.br/"+titleEdited).get();
            Elements info = doc.select( "div#resultadoBusca")
                    .select("div.box_lista_busca_vertical")
                    .select("div.detalhes")
                    .select("a");
            Element firstBook = info.first();
            if(firstBook != null){
                Document book = Jsoup.connect("https://www.skoob.com.br/"+firstBook.attr("href")).get();
                Elements ratingElement = book.select("div#pg-livro-box-rating")
                        .select("span.rating");
                rating = Double.parseDouble(ratingElement.text());
            }
        }catch (Exception e){
        }
        finally {
            return rating;
        }
    }

Enter fullscreen mode Exit fullscreen mode

I won't go into much detail about Jsoup, but basically it will access a web page and return its html content where we can get its data.
The scrapRatings method will search the desired site based on the book title. And will return your evaluation. If the book is not found, your evaluation will be reset to zero.

If you want to know more about the Jsoup, you can go to this link: https://jsoup.org

Going back to the book insert method, after getting the book rating value, we can call the save method of the repository. This method will perform data persistence. Finally, we return the book that was created.

update


@Override
    public BookResponseDTO update(Long id, BookRequestDTO bookRequest) {
        var book = bookRepository.getReferenceById(id);
        book.update(bookRequest);
        return new BookResponseDTO(book);


    }

Enter fullscreen mode Exit fullscreen mode

The update method will look for a book based on its id (using the getReferenceById method) and based on the data sent by the user, it will update them inside the Book object. So let's go back to the Book class and add the update method:

 public void update(BookRequestDTO bookRequest){
        this.title = bookRequest.title();
        this.author = bookRequest.author();
        this.pages = bookRequest.pages();
        this.isbn = bookRequest.isbn();
        this.rate = bookRequest.rate();
        this.finished = bookRequest.finished();
    }

Enter fullscreen mode Exit fullscreen mode

Finally, we return the response DTO.

delete


@Override
    public void delete(Long id) {
        bookRepository.deleteById(id);
    }

Enter fullscreen mode Exit fullscreen mode

The delete method will only call the deleteById method that belongs to the book repository. In this method we don't need to return anything.

count

@Override
public long count(){
        return bookRepository.count();
    }

Enter fullscreen mode Exit fullscreen mode

The count method will return the count method which belongs to the book's repository.

Well done! We finalized the implementation of the business rule from the repository. We can go to the last step which is creating the routes that will call the repository!


Step 02 - Implementing the Route Controller

We arrived at the last step of creating our API. So far we have created our DAO class and the DTO records. The first one will serve as a base for the repository to carry out manipulations in the database. The second one will be used for data input and output.

In addition, we also created the Service that will contain the implementation of our system's business rule.

Now we can create the route controller.

Just like we did earlier in creating a controller to present a simple message to the user when accessing a route, we'll create a robust controller.

Inside the controller package you will create a class called BookController which will have the following initial body:

import com.simplelibrary.simplelibraryAPI.service.BookService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("books")
public class BookController {

@Autowired
private BookService bookService;


} 

Enter fullscreen mode Exit fullscreen mode

This time we are injecting the BookService so we can use the methods we created earlier. If you noticed, we are importing the interface we created and not the implementation. But don't worry, Spring will automatically recognize the implementation we made from the interface.

We have to create the following routes:

HTTP Method /
GET books
GET /books/{id}
POST /books
PUT /books/{id}
DELETE /books/{id}

Route to get all books:

@GetMapping
public ResponseEntity<Page<BookResponseDTO>> getAllBooks(@PageableDefault(size=10, sort={"title"}) Pageable pagination) throws IOException {
    var page = bookService.getAllBooks(pagination);
    return ResponseEntity.ok(page);
}

Enter fullscreen mode Exit fullscreen mode

The route to get all the books will be accessed through a GET request on the URL http://localhost:8080/books. When performing the request, the getAllBooks method will be called.

Note that in the method's signature we are adding an object of type Pageable called pagination in the parameter. Associated with it there is the signature @PageableDefault(size=10, sort={"title"}), this object will allow us to define the pagination properties of my result (we passed this object inside the Service, remember?).

We are injecting the Pageable inside our method, we don't need to worry about creating your object or anything, Spring will do it for you! As per the project requirements we have the following:

  1. As a user, I would like to get all the books registered in the system through pagination. Having only 10 books per page and sorting by title.

We are placing inside the annotation the size properties in which we specify how many books we want to be displayed per page. And the sort property which specifies what the ordering criteria will be, in this case it is title.

We will call the getAllBooks method of the Service we implemented and finally we return the ResponseEntity with the http code response 200, and the pagination in its body.

Route to get the information of a single book:

@GetMapping("/{id}")
public ResponseEntity show(@PathVariable Long id){
    var book = bookService.show(id);
    return ResponseEntity.ok(book);
}
Enter fullscreen mode Exit fullscreen mode

The route to get the information of a single book will be accessed through a GET request on the URL http://localhost:8080/books/{id}. When performing the request, the show method will be called.

Note that for us to know which book the user would like to see the information, they need to specify the id. Therefore, the value of their id is passed in the URL. For this value to be recognized, we put the @PathVariable annotation in the parameters of the method and then its type and the variable where its value will be stored.

We will call the show method of the service and return the ResponseEntity with the http code 200.

Route to store a book


@PostMapping
    public ResponseEntity store(@RequestBody @Valid BookRequestDTO bookRequest, UriComponentsBuilder uriBuilder) throws IOException {
        var book = bookService.store(bookRequest);
        var uri = uriBuilder.path("/books/{id}").buildAndExpand(book.getId()).toUri();
        return ResponseEntity.created(uri).body(book);
    }

Enter fullscreen mode Exit fullscreen mode

The path to store a book will be accessed via a POST request to the URL http://localhost:8080/books/. When performing the request, the store method will be called.

In this method, the user will send in the body of the request (@RequestBody) the fields that we inform in the BookRequestDTO record and where we put all the validations. For these validations to be "called" we need to put the @Valid annotation.
We are also injecting the UriComponentsBuilder into the parameters, which will be used in the response return.

If all validations are successful, the store method of the Service will be called and the book object will be returned.
It is a good API practice, when storing an object in the database, to add a URL to the response headers where the user can access to obtain information about the object that was created. This URL is obtained from the following snippet:


var uri = uriBuilder.path("/books/{id}").buildAndExpand(book.getId()).toUri();

Enter fullscreen mode Exit fullscreen mode

Finally, it is also a good practice to fully return the object that was created in the request body. Therefore, we will not return a DTO object, but a Book type object. In addition to returning status 201 (created).

Route to update a book


@PutMapping("/{id}")
    public ResponseEntity update(@PathVariable Long id, @RequestBody @Valid BookRequestDTO bookRequest){
    var book = bookService.update(id, bookRequest);
    return ResponseEntity.ok(book);

}

Enter fullscreen mode Exit fullscreen mode

The route to update a book will be accessed through a PUT request in the URL http://localhost:8080/books/id. When performing the request, the update method will be called.

This method is similar to the store method, only this time the user has to pass the book's id in the URL.

After calling the update method of the service, we will return the updated book with http status 200.

Route to delete a book

@DeleteMapping("/{id}")
    public ResponseEntity delete(@PathVariable Long id){
    bookService.delete(id);
    return ResponseEntity.noContent().build();
}
Enter fullscreen mode Exit fullscreen mode

The route to delete a book will be accessed through a DELETE request in the URL http://localhost:8080/books/id. When performing the request, the delete method will be called.

This method only receives the id of the book and later the delete method of the service will be called where the book will be successfully deleted.

In this case, we are returning http code 204 (no content).


What we learned in this tutorial

Well done! We have finished creating our Rest API using Spring Boot. You can use Insomnia to test the requests and see if everything is working as expected.

In the next part of the tutorial, you will learn how to create unit and integration tests on your application!

You can find the full project HERE

Top comments (0)