DEV Community

loading...
Cover image for Build a Spring Boot ReST API

Build a Spring Boot ReST API

fastcodeinc profile image fastcode-inc Originally published at getfastcode.com Updated on ・9 min read

Welcome back! In the first blog article, we developed a simple Spring Boot application that prints “Hello World” to the console. In this article, we will build a ReST API that can be called by the API clients.

In a typical Spring Boot application, you will have a relational database with a number of tables that are related to each other. In this article, to keep things simple, we will focus on an application with a single table. In the next article, we will extend this example to include multiple tables and their relationships.

Let’s imagine that we are building a simple Timesheet application.
This application allows employees to enter the hours they worked daily on different tasks and submit the timesheet every two weeks.

In this article, we will focus on building a ReST API for just the Customer table.

Let’s assume that the Customer table in the database includes the following fields:

*customerid
*name
*description
*isActive

Our goal is to develop a ReST API that can be called to perform the following operations:

*Create a new customer
*Get a list of all the customers
*Update an existing customer
*Delete an existing customer

The above operations are known as C.R.U.D (Create, Read, Update, Delete) operations.

Step-1:

First, we need to understand some basic concepts on how we can manipulate the data in a database from a Java/Spring Boot application. This can be done either through JDBC (Java Database Connectivity) API that is a part of the Java Standard Edition or through JPA (Java Persistence API; now renamed to Jakarta Persistence) that is a part of the Java Enterprise Edition.

The Spring framework provides further abstractions on these APIs to make it easier for developers to use these APIs. In this article, we will focus on using JPA.

JPA describes the management of relational data in enterprise Java applications. The Hibernate Object Relational Mapper (ORM) is one of the implementers of the Java Persistence API. In this application, we will be using Hibernate ORM and it’s JPA implementation.

In JPA, a database table is represented as a Java Class. JPA provides a mechanism to persist objects of this Java class to the database table and the rows of the database table back to objects of this Java class. There are a number of rules that are defined by JPA on how an entity needs to be defined. According to the JPA 2.0 specification, these rules are as follows:

  1. The class must be annotated with the @Entity (javax.persistence.Entity) annotation.

  2. Every entity class must have a primary key that uniquely identifies the entity. The primary key is annotated with @id annotation

  3. The entity class must have a no-arg constructor. It may have other constructors as well. The no-arg constructor must be public or protected.

  4. The entity class must be a top-level class. An enum or interface must not be designated as an entity.

  5. The entity class must not be final. No methods or persistent instance variables of the entity class may be final.

  6. Persistent instance variables must be declared private, protected, or package-private and can be accessed directly only by the entity class’s methods. Clients must access the entity’s state through accessor or business methods.

  7. If an entity instance is to be passed by value as a detached object (e.g., through a remote interface), the entity class must implement the Serializable interface.

  8. Both abstract and concrete classes can be entities. Entities may extend non-entity classes as well as entity classes, and non-entity classes may extend entity classes.

Using the rules above, let’s define our Customer entity as follows:

package com.example.demo;

import javax.persistence.*;
import java.io.Serializable;

@Entity
@Table(name = "customer")
public class CustomerEntity implements Serializable {

   public CustomerEntity() {}

 @Id
 @GeneratedValue(strategy = GenerationType.IDENTITY)
 @Column(name = "customerid", nullable = false)
 private Long customerid;

 @Basic
 @Column(name = "name", nullable = true)
 private String name;

 @Basic
 @Column(name = "description", nullable = true)
 private String description;

 @Basic
 @Column(name = "isactive", nullable = true)
 private Boolean isActive;

 public Long getCustomerid() {
   return customerid;
 }

 public void setCustomerid(Long customerid) {
   this.customerid = customerid;
 }

 public String getName() {
   return name;
 }

 public void setName(String name) {
   this.name = name;
 }

 public String getDescription() {
   return description;
 }

 public void setDescription(String description) {
   this.description = description;
 }

 public Boolean getIsActive() {
   return isactive;
 }

 public void setIsActive(Boolean isactive) {
   this.isActive = isActive;
 }
}
Enter fullscreen mode Exit fullscreen mode

Points to note:

  1. The @id annotation defines the primary key. We can generate the identifiers in different ways which are specified by the @GeneratedValue annotation. We can choose from the following Id generation strategies: AUTO, TABLE, SEQUENCE, or IDENTITY. If we don't specify a value explicitly, the generation type defaults to AUTO. When we use the IDENTITY generation type, the database auto-increments the Id values

  2. It is also important to understand the different access types in JPA. This article provides more information on the access types. In the customer entity above, we are using Field access type because we are declaring the @id annotation on a field rather than on the get access method.

  3. If we do not use the @Table annotation, the name of the entity will be considered the name of the table. In such cases, we can omit the @Table annotation. In most cases, the name of the table in the database and the name of the entity will not be the same. In these cases, we can specify the table name using the @Table annotation

  4. The attributes of the @Column annotation include it’s name, length, nullable, and unique. The name attribute specifies the name of the table column. The length attribute specifies the column length. The nullable attribute specifies whether or not the column is nullable, and the unique attribute specifies whether or not the column is unique. If we don't specify this annotation, the name of the field will be considered the name of the column in the table.

  5. JPA supports Java data types as persistable fields of an entity, often known as the basic types. A field or property can be marked with @basic annotation, which is optional. The @basic annotation has two attributes - optional (true, false) and fetch (EAGER, LAZY). By default, optional is set to true and fetch is set to EAGER loading. When optional is set to true, the field/property will accept null values. Lazy loading will only make sense when we have a large Serializable object mapped as a basic type. The JPA specification strictly limits the Java types that can be marked as basic to the following:

Java primitive types (boolean, int, etc)
wrappers for the primitive types (java.lang.Boolean, java.lang.Integer, etc)
java.lang.String
java.math.BigInteger
java.math.BigDecimal
java.util.Date
java.util.Calendar
java.sql.Date
java.sql.Time
java.sql.Timestamp
byte[]
Byte[]
char[]
Character[]
enums
any other type that implements Serializabl

N-layered Architecture

To build a CRUD API for the Customer database table, we will be using a n-layered architecture. This architecture is not necessary for an application with a single database table, but becomes necessary as the number of tables increase and more business logic is added to the application.

The diagram below shows the layers of this architecture:

Alt Text

Domain layer: The domain layer consists of domain classes. In our sample CRUD application, these domain classes are equivalent to the JPA entity classes, which is a single class named CustomerEntity.java that we previously described.

Repository layer: The repository layer in a Spring Boot application consists of Spring Data JPA repositories. Spring Boot makes it very simple to develop these repositories that can enable you to perform basic CRUD operations on an entity. Here’s the JPA repository for the customer entity:

@Repository("customerRepository")
public interface ICustomerRepository extends JpaRepository<CustomerEntity, Long>{}
Enter fullscreen mode Exit fullscreen mode

All we need to do to create a repository is to define an interface that extends the JpaRepository interface, passing in the name of the entity and the data type for the primary key of the entity.

Spring Boot automatically provides an implementation class for this interface. All the methods specified in the JpaRepository interface at this link are therefore available for us to use. Also note that this interface is annotated with @Repository interface. More details on this and other commonly used annotations can be found in this article.

Application Services layer: This layer is used for two purposes: (a) provide transactional support and (b) convert from an entity to a Data Transfer Object (DTO) and vice-versa.

An application transaction is a sequence of application actions that are considered a single logical unit by the application. For a more detailed explanation of Spring transactions, please refer to these series of posts and this other post for an in-depth look at the pitfalls.

DTOs are important for separation of concerns - separating what’s stored in the database from what’s returned to the client of the API. An entity stores what’s stored in the database, whereas a DTO stores what is returned back to the API client. For a more detailed explanation on why DTOs are necessary, please refer to this post.

Again sticking with the separation of concerns principle, we define both an interface and an implementation class in the Application Services layer for the customer entity. This layer will call the JPA repository layer we previously defined.

ICustomerAppService.java

public interface ICustomerAppService {

//CRUD Operations
CreateCustomerOutput create(CreateCustomerInput customer);

void delete(Long id);

UpdateCustomerOutput update(Long id, UpdateCustomerInput input);

FindCustomerByIdOutput findById(Long id);

}
Enter fullscreen mode Exit fullscreen mode

CustomerAppService.java

@Service("customerAppService")
public class CustomerAppService implements ICustomerAppService {

 @Qualifier("customerRepository")
 @NonNull
 protected final ICustomerRepository _customerRepository;

 @Qualifier("ICustomerMapperImpl")
 @NonNull
 protected final ICustomerMapper mapper;

 @NonNull
 protected final LoggingHelper logHelper;

 public CustomerAppService(@NonNull ICustomerRepository 
 _customerRepository, @NonNull ICustomerMapper mapper, 
 @NonNull LoggingHelper logHelper) {
 this._customerRepository = _customerRepository;
 this.mapper = mapper;
 this.logHelper = logHelper;
 }

 @Transactional(propagation = Propagation.REQUIRED)
 public CreateCustomerOutput create(CreateCustomerInput input) {

 CustomerEntity customer = mapper.createCustomerInputToCustomerEntity(input);
 CustomerEntity createdCustomer = _customerRepository.save(customer);
 return mapper.customerEntityToCreateCustomerOutput(createdCustomer);
 }

 @Transactional(propagation = Propagation.REQUIRED)
 public UpdateCustomerOutput update(Long customerId, 
 UpdateCustomerInput input) {

 CustomerEntity customer = mapper.updateCustomerInputToCustomerEntity(input);
 CustomerEntity updatedCustomer = _customerRepository.save(customer);
 return mapper.customerEntityToUpdateCustomerOutput(updatedCustomer);
 }

 @Transactional(propagation = Propagation.REQUIRED)
 public void delete(Long customerId) {

 CustomerEntity existing = _customerRepository.findById(customerId).orElse(null);
 _customerRepository.delete(existing);
 }

 @Transactional(propagation = Propagation.NOT_SUPPORTED)
 public FindCustomerByIdOutput findById(Long customerId) {

 CustomerEntity foundCustomer = _customerRepository.findById(customerId).orElse(null);
 if (foundCustomer == null) return null;
 return mapper.customerEntityToFindCustomerByIdOutput(foundCustomer);
Enter fullscreen mode Exit fullscreen mode

Points to Note:

  1. CRUD methods are defined in the application service class

  2. The class is annotated with @Service annotation. To understand this annotation, please refer to this article.

  3. The application service class depends on three other classes - Customer Repository, Customer Mapper, and Logging Helper. These dependencies are injected into the Application Service class using Spring Dependency Injection mechanism.

Spring allows us to inject dependencies using three methods - constructor-based injection, setter-based injection, or field-based injection. As explained in this article, Constructor-based injection is the preferred way of injecting dependencies.

  1. In constructor-based injection, the dependencies required for the class are provided as arguments to the constructor as seen in the Application Service above.

  2. @Transactional annotation is used at a method-level on public methods

  3. We define and use the following DTOs. Every method, where appropriate, takes an input DTO and returns an output DTO

*CreateCustomerInput
*CreateCustomerOutput
*UpdateCustomerInput
*UpdateCustomerOutput
*FindCustomerByIdOutput

We use a mapping library, MapStruct, to automatically map an entity to a DTO and vice-versa.

ReST Controller layer: This layer is used to present an interface for clients that want to perform CRUD operations on the data on the database tables using http protocol. In our sample application, we have a single ReSTController because we have a single entity/table.

@RestController
@RequestMapping("/customer")
public class CustomerController {

 @Qualifier("customerAppService")
 @NonNull
 protected final ICustomerAppService _customerAppService;

 @Qualifier("projectAppService")
 @NonNull
 protected final IProjectAppService _projectAppService;

 @NonNull
 protected final LoggingHelper logHelper;

 @NonNull
 protected final Environment env;

 public CustomerController(@NonNull ICustomerAppService _customerAppService, @NonNull IProjectAppService 
 _projectAppService, @NonNull LoggingHelper logHelper, @NonNull Environment env) {
 this._customerAppService = _customerAppService;
 this._projectAppService = _projectAppService;
 this.logHelper = logHelper;
 this.env = env;
 }

 @RequestMapping(method = RequestMethod.POST, consumes = { "application/json" }, produces = { "application/json" })
 public ResponseEntity<CreateCustomerOutput> create(@RequestBody @Valid CreateCustomerInput customer) {
 CreateCustomerOutput output = _customerAppService.create(customer);
 return new ResponseEntity(output, HttpStatus.OK);
 }

 // ------------ Delete customer ------------
 @ResponseStatus(value = HttpStatus.NO_CONTENT)
 @RequestMapping(value = "/{id}", method = RequestMethod.DELETE, 
 consumes = { "application/json" })
 public void delete(@PathVariable String id) {
 FindCustomerByIdOutput output = _customerAppService.findById(Long.valueOf(id));
 Optional
 .ofNullable(output)
 .orElseThrow(
 () -> new EntityNotFoundException(String.format("There does not exist a customer with a id=%s", id))
 );

 _customerAppService.delete(Long.valueOf(id));
 }

 // ------------ Update customer ------------
 @RequestMapping(
 value = "/{id}",
 method = RequestMethod.PUT,
 consumes = { "application/json" },
 produces = { "application/json" }
 )

 public ResponseEntity<UpdateCustomerOutput> update(
 @PathVariable String id,
 @RequestBody @Valid UpdateCustomerInput customer
 ) {
 FindCustomerByIdOutput currentCustomer = _customerAppService.findById(Long.valueOf(id));
 Optional
 .ofNullable(currentCustomer)
 .orElseThrow(
 () -> new EntityNotFoundException(String.format("Unable to update. Customer with id=%s not found.", id))
 );

 customer.setVersiono(currentCustomer.getVersiono());
 UpdateCustomerOutput output = _customerAppService.update(Long.valueOf(id), customer);
 return new ResponseEntity(output, HttpStatus.OK);
 }

 @RequestMapping(
 value = "/{id}",
 method = RequestMethod.GET,
 consumes = { "application/json" },
 produces = { "application/json" }
 )
 public ResponseEntity<FindCustomerByIdOutput> findById(@PathVariable String id) {
 FindCustomerByIdOutput output = _customerAppService.findById(Long.valueOf(id));
 Optional.ofNullable(output).orElseThrow(() -> new EntityNotFoundException(String.format("Not found")));

 return new ResponseEntity(output, HttpStatus.OK);
 }
}
Enter fullscreen mode Exit fullscreen mode

Points to Note:

  1. We are passing in the dependencies using Spring Constructor dependency injection

  2. Every ReST controller method has a @RequestMapping annotation, a method type, and the content-type the method consumes and produces.

  3. Where we return back a response, we are using a ResponseEntity
    Although it’s not apparent from the code above, the incoming JSON content is translated into a Java object using the Jackson Mapping library

In this blog article, we have seen how to build a Spring Boot ReST API for a single table. However, in the real-world, an API will be built using multiple tables that are related to each other. In the next (third) blog article, we will discuss how we can handle multiple tables.

Discussion (0)

pic
Editor guide