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();
}
The
getAllBooks
method will be used to get all the books in the database. It will receive as a parameter an object of typePageable
which is responsible for pagination. Later when we implement the route, we'll talk about this class. Finally, it will return an object of typePage
that will contain the contents of theBookResponseDTO
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 theBookResponseDTO
.The
store
method will be used to store a book in the database. It will receive the information from theBookRequestDTO
and return theBook
. 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 theBookResponseDTO
.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;
}
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;
}
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));
}
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;
}
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();
}
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>
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;
}
}
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);
}
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();
}
Finally, we return the response DTO.
delete
@Override
public void delete(Long id) {
bookRepository.deleteById(id);
}
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();
}
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;
}
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);
}
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:
- 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);
}
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);
}
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();
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);
}
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();
}
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)