Spring boot is widely considered an entry to the Spring ecosystem, simple yet effective and powerful! Spring boot makes it possible to create Spring-based stand-alone, quality production apps with minimum needed configuration specifications. Let's keep it simple, just like the movies list API we're about to create!
Before we start
Your project structure matters, Spring boot gives you all the freedom to structure your project the way you see fit, you may want to use that wisely.
There's not a particular structure you need to follow, I'll be introducing mine on this project but that doesn't mean you have to follow it the way it is, as I said you're free!
Most tech companies, including mine, use guiding dev rules to keep the code clean and pretty, the rules are there to help you, don't go rogue.
Phase 0 : Introduction to the global architecture
Before we get busy with the layers specifications, let's have a bird's-eye view :
Phase I : Model definition
Models are the data structure which you're project is dealing with (example: you're building a user management system, the user is your model among others).
Usually, the software architect will be there to provide you with the needed resources to build your Spring boot models, those resources mainly are UML class diagrams. Here's ours :
NOTE: I decided for this model layer to be as simple as possible, to not give a wall to climb instead of a first step. In real-life projects, things get ugly and much more complicated.
Phase II : Service definition
After the model layer, now it's time to blueprint our services' structure, in other words, we need to define what service our API is going to mainly provide.
The services usually are fully invocated by the controller layer which means they usually have the same structure, but not all the time.
This one also will be provided by the software architect, and it comes as a detailed list with descriptions, a set of UML sequence diagrams, or both.
We need our API to be able to provide us with a list of saved movies, save, update and delete a movie.
Our end-points will be the following:
- Save EP: This allows saving a movie by sending a JSON object using an HTTP/POST request.
- Update EP: This allows updating a movie by sending a JSON object using an HTTP/PUT request.
- Delete EP: This allows deleting a movie by sending its id using an HTTP/DELETE request.
- Find all EP: This allows retrieving all the existing movies by sending an HTTP/GET request. (It's not recommended to open a find all end-point because it will slow down your application, instead set a limit, Example: 10 movies per request).
Why are we using multiple HTTP verbs?
If I were in your shoes right now, I'd be asking the same question. First, you can use it as you want with no restrictions, use an HTTP/POST request to retrieve movies, yes you can do that but, there's a reason why we don’t.
Dev rules and conventions are there to help you and keep things in order, otherwise, all we'll be building is chaos!
HTTP verbs are meant to describe the purpose of the request:
HTTP Verb | CRUD Op | Purpose | On success | On failure |
---|---|---|---|---|
POST | Create | Creates a resource | 201 | 404 | 409 |
GET | Read | Retrieves a resource | 200 | 404 |
PUT | Update | Updates (replaces) a resource | 200 | 204 | 404 | 405 |
PATCH | Update | Updates (modifies) a resource | 200 | 204 | 404 | 405 |
DELETE | Delete | Deletes a resource | 200 | 404 | 405 |
Each request requires a response, in both success and error scenarios, HTTP response codes are there to define what type of response we are getting.
HTTP response code | Meaning |
---|---|
100 - 199 | Informational responses |
200 - 299 | Successful responses |
300 - 399 | Redirection responses |
400 - 499 | Client error responses |
500 - 599 | Server error responses |
Know more about HTTP response codes here
Phase III: Technical benchmarking
Now, we need to define the project technical requirements, what communication protocol we should use, what Database is suitable to store data, etc.
We will be using REST as our main communication protocol, H2/file database to store our data.
I chose those technologies for instructional purposes so that I could offer a simple yet working example, but in real-life projects, we select technologies only on the basis of their utility and ability to aid in the development of our API. That was the second factor that influenced my decision.
NOTE: The more technology experience you have, the more accurate your decisions will be.
Phase IV: Development
Github repository initialization
To make this code accessible for all of you, and insure its version, we'll be using git and GitHub, find all the code here : Github Repo
Spring Initializr
Spring provides us with an Initialization suite that helps us start our project quicker than usual.
After the init and downloading the basic version of our project, there're three important files you need to know about :
1. The application main class (aka. the backbone)
This class is the main class of the project and it represents its center.
NOTE: All packages should be under the main package of your application so they can be bootstrapped.
package io.xrio.movies;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class
MoviesApplication {
public static void main(String[] args) {
SpringApplication.run(MoviesApplication.class, args);
}
}
2. The application properties file
This file is a way to put your configuration values which the app will use during the execution (example: URL to the database, server's port, etc.).
NOTE: You can add more properties files, I'll be covering that up in an upcoming article.
NOTE1: By default the file is empty.
spring.datasource.url=jdbc:h2:file:./data/moviesDB
spring.jpa.defer-datasource-initialization=true
spring.datasource.driverClassName=org.h2.Driver
spring.datasource.username=sa
spring.datasource.password=password
spring.jpa.hibernate.ddl-auto=update
3. The pom file
Since we are using Maven as our dependency manager (you can use Gradle instead) we have a pom.xml file containing all the data about our dependencies.
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.5.4</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>io.xrio</groupId>
<artifactId>movies</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>movies</name>
<description>Just a simple movies api</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
</project>
Building the model layer
The model layer is the direct implementation of our class diagrams as Java classes.
package io.xrio.movies.model;
import lombok.Data;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import java.util.Date;
@Data
@Entity
public class Movie {
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE)
private Long id;
private String name;
private String type;
private Long duration;
private Long releaseYear;
}
The
provides us with embedded constructors and accessors so we can reduce the written code. see more about the [Lombok project](https://projectlombok.org/).
The
```@Entity```
is there to tell Spring Data that particular class should be represented as a table in our relational database. we call that process an ORM (Object Relational Mapping).
So that our id won’t be duplicated, we will use a sequence, we put the
```@GeneratedValue(strategy = GenerationType.SEQUENCE)```
there.
### Building the repository facade
Simply, an interface that will inherit Spring Data JPA powers and handle the CRUD ORM operations on our behalf.
package io.xrio.movies.repository;
import io.xrio.movies.model.Movie;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
@Repository
public interface MoviesRepository extends JpaRepository {
}
@Repository ensures the dependency injection of the repository bean among other things, since the interface inherits from the generic JpaRepository, it will have all the already-set mechanisms of Spring Data JPA.
Spring Data is smart enough at the level it can handle a new operation only from the function signature. Example: You need to file a movie by its title? No problem, just add this function to your interface :
Movie findByName(String name);
### Building the custom exception
Before building our service layer, we need a couple of custom exceptions that will be thrown when things go south.
Basing on our service schema only two exceptions can be thrown when:
- Creating an already existing movie (MovieDuplicatedException class).
package io.xrio.movies.exception;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
- @author : Elattar Saad
- @version 1.0
-
@since 10/9/2021
*/
@EqualsAndHashCode(callSuper = true)
@data
@AllArgsConstructor
public class MovieDuplicatedException extends Exception{/**
- The duplicated movie's id */ private Long mid;
}
- Updating a non-existing movie (MovieNotFoundException class).
package io.xrio.movies.exception;
import lombok.Data;
import lombok.EqualsAndHashCode;
@EqualsAndHashCode(callSuper = true)
@data
@AllArgsConstructor
public class MovieNotFoundException extends Exception{
/**
* The nonexistent movie's id
*/
private Long mid;
}
***NOTE: There're mainly four ways to handle exception in Spring boot.***
***NOTE1: You can generalize custom exceptions so they can be used for more than one model.***
***NOTE2: You can handle your exceptions without custom-made exceptions and handlers.***
### Building the service layer
First, we will create a movie service interface that will bear the schema of our service, then implement it with the needed logic. This helps us to achieve the [purpose of the IoC](https://en.wikipedia.org/wiki/Inversion_of_control).
package io.xrio.movies.service;
import io.xrio.movies.exception.MovieDuplicatedException;
import io.xrio.movies.exception.MovieNotFoundException;
import io.xrio.movies.model.Movie;
import java.util.List;
public interface MovieService {
Movie save(Movie movie) throws MovieDuplicatedException;
Movie update(Movie movie) throws MovieNotFoundException;
Long delete(Long id) throws MovieNotFoundException;
List<Movie> findAll();
}
package io.xrio.movies.service.impl;
import io.xrio.movies.exception.MovieDuplicatedException;
import io.xrio.movies.exception.MovieNotFoundException;
import io.xrio.movies.model.Movie;
import io.xrio.movies.repository.MoviesRepository;
import io.xrio.movies.service.MoviesService;
import lombok.Data;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
@data
public class MovieServiceImpl implements MovieService {
final MovieRepository movieRepository;
@Override
public Movie save(Movie movie) throws MovieDuplicatedException {
Movie movieFromDB = movieRepository.findById(movie.getId()).orElse(null);
if (movieFromDB != null)
throw new MovieDuplicatedException(movie.getId());
return movieRepository.save(movie);
}
@Override
public Movie update(Movie movie) throws MovieNotFoundException {
Movie movieFromDB = movieRepository.findById(movie.getId()).orElse(null);
if (movieFromDB == null)
throw new MovieNotFoundException(movie.getId());
movie.setId(movieFromDB.getId());
return movieRepository.save(movie);
}
@Override
public Long delete(Long id) throws MovieNotFoundException {
Movie movieFromDB = movieRepository.findById(id).orElse(null);
if (movieFromDB == null)
throw new MovieNotFoundException(id);
movieRepository.delete(movieFromDB);
return id;
}
@Override
public List<Movie> findAll() {
return movieRepository.findAll();
}
}
I combined the use of constructor dependency injection and Lombok’s
```@RequiredArgsConstructor```
to reduce my code, without that it will look like this :
@Service
public class MovieServiceImpl implements MovieService {
final MovieRepository movieRepository;
public MovieServiceImpl(MovieRepository movieRepository) {
this.movieRepository = movieRepository;
}
...
}
### Building the controller layer
After the service layer, time to build our controller to accept incoming requests.
As previously said, the controller layer directly calls the service layer, that's the reason behind the injection of a MovieService bean inside the movie controller.
package io.xrio.movies.controller;
import io.xrio.movies.exception.MovieDuplicatedException;
import io.xrio.movies.exception.MovieNotFoundException;
import io.xrio.movies.model.Movie;
import io.xrio.movies.service.MovieService;
import lombok.Data;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("movie")
@data
public class MovieController {
final MovieService movieService;
@PostMapping("/")
public ResponseEntity<?> save(@RequestBody Movie movie) throws MovieDuplicatedException {
if (movie == null)
return ResponseEntity.badRequest().body("The provided movie is not valid");
return ResponseEntity.status(HttpStatus.CREATED).body(movieService.save(movie));
}
@PutMapping("/")
public ResponseEntity<?> update(@RequestBody Movie movie) throws MovieNotFoundException {
if (movie == null)
return ResponseEntity.badRequest().body("The provided movie is not valid");
return ResponseEntity.ok().body(movieService.update(movie));
}
@DeleteMapping("/{id}")
public ResponseEntity<?> delete(@PathVariable Long id) throws MovieNotFoundException {
if (id == null)
return ResponseEntity.badRequest().body("The provided movie's id is not valid");
return ResponseEntity.ok().body("Movie [" + movieService.delete(id) + "] deleted successfully.");
}
@GetMapping("/")
public ResponseEntity<?> findAll() {
return ResponseEntity.ok().body(movieService.findAll());
}
}
One more last step and we're good to go. To handle the exception thrown from the service layer, we need to catch it in the controller layer.
Fortunately, Spring boot provides us with an Exception Handler that can resolve exceptions under the hood and without any code to the controller.
package io.xrio.movies.controller.advice;
import io.xrio.movies.exception.MovieDuplicatedException;
import io.xrio.movies.exception.MovieNotFoundException;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler;
@ControllerAdvice
public class MovieControllerExceptionHandler {
@ExceptionHandler(MovieNotFoundException.class)
private ResponseEntity<?> handleMovieNotFoundException(MovieNotFoundException exception){
String responseMessage = "The provided movie ["+exception.getMid()+"] is nowhere to be found.";
return ResponseEntity
.status(HttpStatus.NOT_FOUND)
.body(responseMessage);
}
@ExceptionHandler(MovieDuplicatedException.class)
private ResponseEntity<?> handleMovieDuplicatedException(MovieDuplicatedException exception){
String responseMessage = "The provided movie ["+exception.getMid()+"] is already existing.";
return ResponseEntity
.status(HttpStatus.CONFLICT)
.body(responseMessage);
}
}
All we need to do next is to test what we build via a rest client, for this reason, I'm using [Insomnia](https://insomnia.rest/) :
##### The post end-point
<p align="center">
<img src="https://elattar.me/images/spring/movie-test-post.png">
</p>
##### The get end-point
<p align="center">
<img src="https://elattar.me/images/spring/movie-test-get.png">
</p>
##### The put end-point
<p align="center">
<img src="https://elattar.me/images/spring/movie-test-put.png">
</p>
##### The delete end-point
<p align="center">
<img src="https://elattar.me/images/spring/movie-test-delete.png">
</p>
And finally, testing the error scenario for some end-points.
Let's try and save a movie with an existing id :
<p align="center">
<img src="https://elattar.me/images/spring/movie-test-save-error.png">
</p>
Or delete a non-existing movie :
<p align="center">
<img src="https://elattar.me/images/spring/movie-test-delete-error.png">
</p>
## Summary
Spring Boot is a powerful apps making tool, takes away all the times consuming tasks and leaves you with the logical ones to handle.
Don't get fouled, building APIs is not that easy, and from what I saw, that's just the tip of the Iceberg!
Find the source code [Here](https://github.com/xrio/simple-spring-boot-movies).
More articles [Here](https://elattar.me/).
Top comments (0)