REST API with Spring Boot 3— Part 1
Project setup and API implementation with Spring Boot
This is the first article of my REST API series, where I write about my impressions and experience with this architecture style using Java with Spring Boot.
The full series of articles are listed below:
- Part I: Project setup and API implementation with Spring Boot
- Part II: HATEOAS, Root Entry Point and Pagination
- Part III: API documentation using Swagger
- Part IV: API Authentication and Authorization with Spring Security
The implementation of each article is available on GitHub in a specific branch, and the final implementation is on the main branch.
In this article we will go through the initial project setup, understand the key concepts and implement the first version of the REST API using known best practices and my personal approach.
The code of this article is available on GitHub in branch ‘ch-01’.
REST API
REST (REpresentational State Transfer) is an software architectural style for providing standards between systems on the web, making it easier to communicate with each other. REST was defined by Roy Fielding in his 2000 PhD dissertation.
By using REST architectural style it is possible to separate the concerns of client and server, which can be developed independent of each other.
This article assumes previous basic knowledge about HTTP and REST architecture. Nevertheless, let’s review some key definitions:
Resource is anything on the Web that can be identified, named or addressed by an URI (Uniform Resource Identifier). The URL (Uniform Resource Locator) of the resource is also know as its endpoint. The operations (or state transitions) on a given resource are performed using resource methods; and when it is used the HTTP protocol they are related to HTTP Methods (or Verbs). The most common used HTTP methods are GET, POST, PUT, DELETE for retrieve, create, update or delete an identified resource. An architectural API that complies to all REST specifications and constraints are know as Restful APIs.
In summary, when a REST endpoint receives a request, the server will transfer to the client a representation of the state of the that resource.
There are six guiding architectural constraints that defines a RESTful system, which provides this desired properties of performance, scalability, simplicity, visibility, portability, modifiability and reliability:
- Client-Server: this constraints is associated with separation of concerns, so the client and server can evolve independently. The interaction between them is by requests initiated by the client and server responses to those requests.
- Stateless: it means that there is no “client-session” in the server, so no client context are stored between requests. Every request contains all the information that allows the server perform the operation and return a response.
- Cacheable: cache requires that the data within a response to a request to be labeled (implicitly or explicitly) as cacheable or non-cacheable. So, for a cacheable response, the client can manage the data to prevent unnecessary new requests to the server, reusing previous response data (if not expired).
- Layered System: between the client request and the server response, there may exist many (or none) intermediary servers/layers, which can handle load balancing, cache and security logic for instance. The point here is that the client does not have to know how many layers or servers are there to generate a response from a resource request.
- Uniform Interface: this constraint simplifies and decouples the architecture, so each part act independently. There are four inner constraints for this uniform interface as follows: a) Resource identification in requests (a request has to include a resource identifier); b) Resource manipulation through representations (response includes metadata about the resource so the client can modify the resource); c) Self-descriptive messages (each request or response message contains all the information that is needed to be understood); d) Hypermedia As The Engine Of Application State — HATEOAS (server responses provide links dynamically to discover all the available resources it needs).
- Code on Demand: this constraint is optional and means that the server can transfer executable code to client, usually in the form of scripts.
Project Setup
To walk through the concepts, here we will build a simple REST API to register Books using Java and Spring Boot (v3.x.x) framework.
Let’s use the Spring Initializer to create our project. We will use Java 17 maven based project and will need the following starting dependencies:
- Spring Web: for Web/RESTful features and embedded web container;
- Spring Data JPA: to easily persist data on JPA based repositories;
- Spring Boot DevTools: provides additional development-time features, like fast application restarts;
- Bean Validation: to automatically apply validation to mapped values;
- H2 Database: in-memory database to store data;
- Lombok: to avoid typing boiler-plate code.
We will add afterwards the ModelMapper dependency too for automatic mapping attributes between classes.
As usual, we need to download the generated zip file and open all the projects in our favorite IDE (I’ll be using IntelliJ IDEA).
Dependencies section of pom.xml will be as follows:
<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-validation</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</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.modelmapper</groupId>
<artifactId>modelmapper</artifactId>
<version>3.0.0</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
And finally, in the application.yml file, let’s add the configuration to access the in-memory H2 database, some hibernate configs and set the application to run on port 9090:
server:
port: 9090
servlet:
context-path: /api/v1
error:
include-stacktrace: never
include-message: never
spring:
datasource:
url: jdbc:h2:mem:booksdb
username: admin
password: admin
driverClassName: org.h2.Driver
jpa:
defer-datasource-initialization: true # to allow DB initialization scripts
database-platform: org.hibernate.dialect.H2Dialect
show-sql: true
properties:
hibernate:
format_sql: true
hibernate:
ddl:
auto: update
h2:
console:
enabled: true
jackson:
default-property-inclusion: NON_NULL
Domain classes
First of all, I think it’s important to model the business domain. In this example, it will be very simple, just a ‘Book’ class to represent a registered book.
It’s also expected that each book may have some “Reviews”, but here we will represent the review as a string text, and the reviews as a list of java.lang.String class, with no need for specific model class.
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Book {
private Integer id;
private String title;
private String language;
private Integer yearOfPublication;
private String authors;
}
Controller
Once defined the domain model, I like to create the REST Controller classes for the defined resources, to get forced to think of what operations the API will provide and which endpoints will be necessary. Just to make it work/compile, it can be implemented mock responses for each candidate endpoint just to validate them.
The Controllers classes, in my opinion, should be only responsible for accept and validate request data, call business services classes and encapsulate resulting data into appropriate responses. I try very hard not to add business logic in this layer, keeping it like a clean HTTP façade.
I think it is also a good practice to create specific Request and Response serializable DTOs (Data Transfer Objects) for transferred data representation over http, to allow segregation of json-parsing and attributes validation, leaving our model as clean as possible.
It’s a good practice to include API version in the resource URI, for better API management like this: “http://server:port/api/v1/books/…”
The section below in the ‘application.yml’ files ensures this versioning to API URL:
server:
servlet:
context-path: /api/v1
For each designed operation, it’s important to use the proper HTTP method: GET for retrieve resource data, POST to create resource, PUT/PATCH for updates and DELETE to remove resource.
The controller layer here will have the endpoints described below for complete CRUD operations of Book resource, and to insert and retrieve book reviews:
// BooksController endpoints (methods implementation omitted)
@GetMapping
public ResponseEntity<List<BookResponse>> getBooks()
@GetMapping("/{id}")
public ResponseEntity<BookResponse> getBook(@PathVariable("id") Integer id)
@PostMapping
public ResponseEntity<BookResponse> createBook(@RequestBody @Valid BookRequest request)
@PutMapping("{id}")
public ResponseEntity<BookResponse> updateBook(@PathVariable("id") Integer id, @RequestBody @Valid BookRequest request)
@DeleteMapping("{id}")
public ResponseEntity deleteBook(@PathVariable("id") Integer id)
@GetMapping("{bookId}/reviews")
public ResponseEntity<List<ReviewResponse>> getReviews(@PathVariable("bookId") Integer bookId)
@PostMapping("{bookId}/reviews")
public ResponseEntity<ReviewResponse> createReview(@PathVariable("bookId") Integer bookId, @RequestBody @Valid ReviewRequest request)
The ‘@Valid’ annotation indicates to only accept valid Request DTOs after applying the validations mapped with bean-validations in the mapped attributes.
@Data
@NoArgsConstructor
@AllArgsConstructor
public class BookRequest {
@NotEmpty(message = "{required.field}")
@Size(min = 1, max = 200, message = "{invalid.field}")
String title;
@NotEmpty(message = "{required.field}")
@Size(min = 1, max = 50, message = "{invalid.field}")
String language;
@PositiveOrZero(message = "{invalid.field}")
Integer yearOfPublication;
@Size(min = 1, max = 200, message = "{invalid.field}")
String authors;
}
@Data
@NoArgsConstructor
@AllArgsConstructor
public class ReviewRequest {
@NotEmpty(message = "{required.field}")
@Size(min = 1, max = 200, message = "{invalid.field}")
String review;
}
We will overwrite the default validation messages here, so we set the message key in the annotation. It is necessary to create the proper message text in the ‘messages.properties’ file, storage under ‘resources’ directory.
invalid.field=Must have valid format
required.field=Must not be empty
It is also necessary to config Spring Beans to set this message property file as the resource bundle message source. In this project we’ll create these beans in the main class:
@Bean
public MessageSource messageSource() {
ReloadableResourceBundleMessageSource messageSource = new ReloadableResourceBundleMessageSource();
messageSource.setBasename("classpath:messages");
messageSource.setDefaultEncoding("UTF-8");
messageSource.setCacheSeconds(10); //reload messages every 10 seconds
return messageSource;
}
@Bean
public LocalValidatorFactoryBean getValidator() {
LocalValidatorFactoryBean bean = new LocalValidatorFactoryBean();
bean.setValidationMessageSource(messageSource());
return bean;
}
My approach is to implement this layer using a mock stub of the service class that returns dummy hard-coded data, and then write the unit tests to validate the expected behavior of each endpoint.
The complete Controller class implementation is available here, and its unit tests are available here.
Error handling
Spring Boot provides exception handlers to give proper error responses, and we can extend and create our own handlers.
Let’s create custom exceptions in this API for the situations where the requested resource does not exist, and for when it tries to create a resource that is already registered. Both will be unchecked exceptions that will have attributes to store request data and provide friendly error messages:
@NoArgsConstructor
@AllArgsConstructor
public class ResourceAlreadyExistsException extends RuntimeException {
private String resourceName;
private String resourceId;
public String getMessage() {
if (resourceName == null || resourceId == null)
return null;
return String.format("Resource '%s' already registered with id '%s'", resourceName, resourceId);
}
}
@NoArgsConstructor
@AllArgsConstructor
public class ResourceNotFoundException extends RuntimeException {
private String resourceName;
private String resourceId;
public String getMessage() {
if (resourceName == null || resourceId == null)
return null;
return String.format("Resource '%s' not found with id '%s'", resourceName, resourceId);
}
}
Let’s also create a standard error response DTO:
@Getter
public class ErrorResponse {
private String title;
private String message;
private Map<String, String> details;
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd hh:mm:ss")
private LocalDateTime timestamp;
}
And now let’s make the magic happen by extending the default exception handler, binding each exception to a ErrorResponse instance and to an HTTP status code. The ‘@ControllerAdvice’ annotation is mandatory to provide this behavior.
@ControllerAdvice
public class RestExceptionHandler extends ResponseEntityExceptionHandler {
@ExceptionHandler(ResourceNotFoundException.class)
public ResponseEntity<Object> handleResourceNotFound(ResourceNotFoundException ex) {
ErrorResponse error = new ErrorResponse(ex.getClass().getSimpleName(), ex.getMessage());
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(error);
}
@ExceptionHandler(ResourceAlreadyExistsException.class)
public ResponseEntity<Object> handleResourceAlreadyRegistered(ResourceAlreadyExistsException ex) {
ErrorResponse error = new ErrorResponse(ex.getClass().getSimpleName(), ex.getMessage());
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(error);
}
@Override
public ResponseEntity<Object> handleMethodArgumentNotValid(MethodArgumentNotValidException ex, HttpHeaders headers, HttpStatusCode status, WebRequest request) {
Map<String, String> fieldErrors = new HashMap<>();
ex.getBindingResult().getAllErrors().forEach((error) -> {
String fieldName = ((FieldError) error).getField();
String errorMessage = error.getDefaultMessage();
fieldErrors.put(fieldName, errorMessage);
});
ErrorResponse error = new ErrorResponse("ValidationException", "Invalid fields", fieldErrors);
return new ResponseEntity<>(error, HttpStatus.BAD_REQUEST);
}
}
The implementation of all classes mentioned are available here.
Services
When using layered packaging, I like to make use of service classes for business logic implementation, providing resulting data to controllers and accessing repository classes if necessary.
So, basically it’s implemented some CRUD operations applying the business logic for each one.
The service layer will be responsible to instantiate model classes and orchestrate then to accomplish the operation requested.
Again my approach is to implement this layer using a mock stub of an repository class that returns dummy hard-coded data, and then write the unit tests to validate the expected behavior of each service method.
@Service
@RequiredArgsConstructor
public class BooksService {
public List<Book> findBooks() {
return booksRepo.findAll();
}
public Book findBookById(Integer id) {
return booksRepo.find(id)
.orElseThrow(() -> new ResourceNotFoundException(Book.class.getSimpleName(), id));
}
public Book createBook(Book book) {
Optional<Book> registeredBook = booksRepo.findBookByTitle(book.getTitle());
if (registeredBook.isPresent())
throw new ResourceAlreadyExistsException(Book.class.getSimpleName(), registeredBook.get().getId());
return booksRepo.save(book);
}
public Book updateBook(Book book) {
abortIfBookDoesNotExist(book.getId());
Book updated = booksRepo.update(book);
return updated;
}
public void deleteBook(Integer id) {
abortIfBookDoesNotExist(id);
booksRepo.delete(id);
}
public List<String> findReviews(Integer bookId) {
abortIfBookDoesNotExist(bookId);
return booksRepo.findReviewsOfBook(bookId);
}
public String addReview(Integer bookId, String review) {
abortIfBookDoesNotExist(bookId);
return booksRepo.addReview(bookId, review);
}
private void abortIfBookDoesNotExist(Integer id) {
booksRepo.find(id).orElseThrow(() -> new ResourceNotFoundException(Book.class.getSimpleName(), id));
}
}
The complete BookService class implementation is available here, and its unit tests are available here.
Repository
In this repository layer, let’s create a representation of a persistent book, and since we are using JPA, let’s create a ‘BookEntity’ to store the book attributes and its reviews:
@Entity
@Table(name = "BOOKS")
@Data
@NoArgsConstructor
@AllArgsConstructor
public class BookEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
private String title;
private String language;
private Integer yearOfPublication;
private String authors;
@ElementCollection(fetch=FetchType.LAZY)
@CollectionTable(name="REVIEWS", joinColumns=@JoinColumn(name="BOOK_ID"))
@Column(name="TEXT")
private List<String> reviews = new ArrayList<>();
}
By using Spring Data JPA, let’s create then a JpaRepository extension interface to automatically inherit CRUD methods to our H2 database. Besides the provided methods by the parent interface, we added a ‘findByTitle’ method.
public interface BookRepository extends JpaRepository<BookEntity, Integer> {
Optional<BookEntity> findByTitle(String title);
}
To provide persistent methods to service layer, I like to create a repository class that accepts and return model instances, and encapsulates the transformation from model classes to entities/persistents classes. So we do not attach JPA entities to the up-level service class.
By using this approach we can provide better segregation between layers and it gets easier to change the persistent mechanism, changing for a NoSQL storage after for example.
So I implemented a ‘BookGatewayRepository’, that has an inject dependency of the proper repository instance (‘BookRepository’ in this case), and usually just converts data between model and entities classes and delegates operations to the repository instance. Again there are unit tests for each operation.
To load initial data in the database, it’s just necessary to create a ‘data.sqldata.sql’ file at ‘resources’ directory.
Mapper class
To decouple layers and classes, I like to use the ModelMapper library that automatically maps an objects to another.
We will need to use this mapping strategy at Controller layer to convert between requests/responses DTOs and model classes, and again in Repository layer to convert between model and JPA entity classes.
To reuse the same instance of this mapper, I like to create an Spring Bean like this (placed in main class):
@Bean
public ModelMapper getModelMapper() {
var mapper = new ModelMapper();
mapper.getConfiguration().setSkipNullEnabled(true);
return mapper;
}
So, we can inject this Bean (using @Autowired) whenever need.
But again I like to create a generic mapper class that act as an abstraction to what library is actually used to provide de mapping feature. So, it is possible to change to MapStruct library if we decided so.
I named this class ‘BookMapper’, which makes use of the ModelMapper Bean created earlier:
@Component
@RequiredArgsConstructor
public class BookMapper {
private final ModelMapper mapper;
public Book toModel(BookEntity entity) {
return mapper.map(entity, Book.class);
}
public BookEntity toEntity(Book book) {
return mapper.map(book, BookEntity.class);
}
public BookEntity copyValues(BookEntity source, BookEntity destination) {
mapper.map(source, destination);
return destination;
}
public Book toModel(BookRequest request) {
return mapper.map(request, Book.class);
}
public BookRequest toRequest(Book book) {
return mapper.map(book, BookRequest.class);
}
public BookResponse toResponse(Book book) {
return mapper.map(book, BookResponse.class);
}
}
Testing
By building the API layer by layer with unit tests, there are reduced risk of critical bugs. I like to think about the interface and method signature, and then use the TDD approach to drive the methods implementations, as it helps me to think about what goals I want to achieve with each operation.
Good unit tests allow us to make changes and refactoring and ensuring that we did not break anything in the application as we evolve them.
Even so, functional tests are also important, and I like to create a collection in Postman tool to ensure regularly API responses. Most of the time I like to implement functional tests with cucumber and rest-assured libraries too.
The complete implementation of the project of this article is available on GitHub in branch ‘ch-01’.