Building a Task Management Application using Rest API, Spring Boot, Maven and Fauna
aidelojep
Posted on November 30, 2021
Written in connection with the Write with Fauna Program.
This article focuses on the tutorial steps in building a Rest API using the Java Programming framework (Spring Boot), Maven, and Fauna. We used Fauna as our database to save our information and integrated this into our Spring Boot project. We also outlined these steps to make it easy for beginners to follow through and implement the same using these steps when working on a similar project.
The Rest API is more suitable for server-side API rendering. Hence, the REST API is a valuable architectural style for microservices because of its simplicity, scalability and flexibility. In microservice architecture, each application is designed as an independent service from the other. We recall that microservices rely on small teams to deploy and scale their respective services independently, this makes the REST API an invaluable resource to this architectural style.
Prerequisites
To fully understand this part of the tutorial, you are required to have the following:
- Fundamental knowledge of how to program with Java.
- At least fundamental knowledge of Spring Framework and Spring Boot.
- Java Development Kit(JDK)installed.
- Postman installed or click on the link to download and install.
- Maven installed or click on the link to download and install.
- IntelliJ installed or click on the link to install. You can use any other IDEA of choice.
What is an API?
In the simplest form, an API is the acronym for application programming interface that allows for two or more different applications to talk to each other. Everytime you make use of these applications, the applications on your phone, gadgets or computer connect to the internet and send data to the server. This data retrieved by the server is then interpreted, and some actions are performed and a feedback is sent back to you in a human or readable format. The API also provides a level of security here since each communication entails a small packet of data, the data sharing here only entails that which is necessary. Another additional benefit of RESTful APIs is its Client-Server constraints. This constraint operates on the concept that the client and server side should be separated from each other. This is referred to as separation of concerns which guarantees more efficiency in our application. Therefore, I should be able to make changes on my client side without affecting my database design on the server and vice-versa. This makes our application to be loosely coupled and easily scalable .
This article teaches how to create a SpringBoot and Restful API that performs CRUD (Create, Read, Update and Delete) operations by making a database call to a Fauna. The application we will be building in this tutorial is a “task-management app” for users to manage all their daily tasks.
Key Takeaways
- How to create and set up a Spring Boot application with a Tomcat Server.
- Fauna database configuration in a Spring Boot Project.
- Maven for Dependency management.
- Exception Handling in Java.
- How to document API using Swagger.
Project Setup
To initialize the project we are going to use spring initializer . Enter the maven project properties of the project including the dependencies as shown below and click on the generate button. This will generate a zip file and download it for you. Unzip it and open it in your favorite IDEA and sync the dependencies with Maven.
For this project we are going to add two dependencies namely:
- Spring web: This dependency makes your project a web application. The spring-boot-starter-web dependency transitively pulls in all dependencies related to Web development using Spring MVC, REST, and Tomcat as a default embedded server.
- Spring Data JPA: This allows us to persist data in SQL databases using Spring Data and Hibernate which is an implementation of the JPA. JPA stands for Java Persistent API, it is a specification that is part of Java EE (Enterprise Edition) and defines an API for Object-Relational Mappings (ORM) and for managing persistent objects and Relational Databases. It is considered a standard approach for Object Relational Mapping. Being that JPA is a specification, it does not perform any operation by itself, as such requires implementation. Hibernate is one of those ORM (Object Relational Mapping) tools that implements JPA. Others include TopLink, MyBatis.
The EntryPoint of the Application
The beauty of SpringBoot lies in how easy it is to create stand-alone, production-grade spring-based applications that you can "just run". If you open your TaskManagerApplication.java file.
package com.taskVentures.taskmanager;
import org.springframework.beans.factory.config.ConfigurableBeanFactory;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Scope;
@SpringBootApplication
public class TaskmanagerApplication {
public static void main(String[] args) {
SpringApplication.run(TaskmanagerApplication.class, args);
}
}
SpringBoot applications should have an entry point class with the public static void main(String[] args) methods, which is annotated with the @SpringBootApplication
annotation and will be used to bootstrap the application. It is the main method which is the entry point of the JVM to run our application.
The @SpringBootApplication
annotation informs the Spring framework, when launched, to scan for Spring components inside this package and register them. It also tells Spring Boot to enable Autoconfiguration, a process where beans are automatically created based on classpath settings, property settings, and other factors. The @SpringBootApplication
annotation has composed functionality from three annotations namely @EnableAutoConfiguration
,@ComponentScan
, and @Configuration
. So we can say it is the shortcut for the three annotations.
Now, we can now run our application. We can do this by either clicking on the play button on our IDEA or running this command: mvn spring-boot:run on our command line. Navigate to the root of the project via the command line and execute the command. Boom! Tomcat started on port 8081 which is the port we configured our application to run.
Maven as a dependency management tool.
The pom.xml file houses the dependencies, Maven plugins in our project.
The dependency section simply contains the dependencies we added to our project namely SpringWeb and springfox for documenting our api.
<?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>com.taskVentures</groupId>
<artifactId>taskmanager</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>taskmanager</name>
<description>A web application that individual to manage their daily task.</description>
<properties>
<java.version>11</java.version>
</properties>
<dependencies>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-boot-starter</artifactId>
<version>3.0.0</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</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>
</plugin>
</plugins>
</build>
</project>
Adding Additional Maven Dependencies
In this section we are going to add additional deficiencies to the project. To do this, we navigate to Maven Repository and search for the Fauna dependency and add it to the dependencies section of the pom.xml file:
- Faunadb: A Fauna cloud database dependencies that connect our Java application to Fuana serverless database.
- Lombok: A lombok dependency helps us to reduce boiler plate codes.
- Sync the newly added dependencies to the application.
- The modified pom.xml should like this:
<!--newly added dependencies-->
<dependency>
<groupId>com.faunadb</groupId>
<artifactId>faunadb-java</artifactId>
<version>2.10.0</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.20</version>
<scope>provided</scope>
</dependency>
Next, we can now proceed to create a database on the Fauna dashboard, and generate a server key and configure FaunaClient in the Spring Boot project. To create a database and server key for our SpringBoot project, we are going to register a Fauna account. To do this, click on this link sign up today and ignore if you have one already. After signup, you get a prompt to create a database like the image below:
Here, we named our database as taskmanager_db. In naming your database, always ensure to use a name that is descriptive. Next we are going to generate our Fauna secret key.
Creating a Fauna API Key
To create a Fauna API Key, you would go to your settings on the Fauna sidebar (at the top left of the screen). This Fauna API key is required to connect the database to our Task_Management_App.
The secret keys generated from Fauna are meant to be copied and stored somewhere safe that can be easily retrieved.
Configuring Fauna Client
In the resources folder within the src/main folder, open application.properties file and add the secret key that you have generated.
fauna-db.secret=”your api secret key should be here”
Next we need to create a bean creates a single instance of the configuration with the help of the @Scope
annotation and inject our api key using the @value
annotation.
@Value("${fauna-db.secret}")
private String serverKey;
@Bean
@Scope(value = ConfigurableBeanFactory.SCOPE_SINGLETON)
public FaunaClient faunaConfiguration() {
FaunaClient faunaClient = FaunaClient.builder()
.withSecret(serverKey)
.build();
return faunaClient;
}
Project Structure
Our project will be structured into four subpackages:
Data: This subpackage will house our Data access layer, which will include our Domain and repository.
- Service: This is where our business logic will be.
- Web: This package will house our controllers.
- Exceptions: This is where all our custom exceptions will be. Throwing exceptions is very important in building a resilient system. This structure will ensure that when a client makes a call to access a resource in the application, such client does not have direct access to our database, rather a request is directed to our controller. Our controller calls the right service(the business logic) which then through our repository makes a call to our database. This architecture also ensures the separation of concerns.
Creating The Domain Class
In the data package, create another package called models. Inside the models package, create a class called Task with the following code:
package com.taskVentures.taskmanager.data.models;
import com.faunadb.client.types.FaunaConstructor;
import com.faunadb.client.types.FaunaField;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
public class Task {
private String id;
@FaunaField
private String name;
@FaunaField
private String description;
@FaunaField
private boolean isCompleted;
@FaunaConstructor
public Task(@FaunaField("id")String id, @FaunaField("name")String name, @FaunaField("description")String description, @FaunaField("isCompleted")boolean isCompleted) {
this.id = id;
this.name = name;
this.description = description;
this.isCompleted = isCompleted;
}
}
- @FaunaField annotation Makes the instance variable annotated as database column
- We have used the @FaunaConstructor annotation to specify our create constructor and give our files values on creation.
- @data creates setters and getters for the class.
- @NoArgsConstructor annotation creates a no argument constructor.
- @AllArgsContructor creates an all argument constructor.
Payloads
Inside the data package, create a package with the name payloads. This package will have two sub-packages “request” and “response” to handle our request payloads and response payloads respectively.
Request payloads
Inside the request package create an EmployeeRequest class with the following code:
package com.taskVentures.taskmanager.data.payloads.requests;
import lombok.Data;
import java.time.LocalDateTime;
@Data
public class TaskRequest {
@NotNull
@NotBlank
private String name;
@NotNull
@NotBlank
private String description;
private boolean isCompleted;
}
@notblank and @NotNull : These two annotation checks and validate the fields where they are mapped to ensure the values are not blank and null respectively.
Response payload
Inside the response package create a TaskResponse class with the following code:
package com.taskVentures.taskmanager.data.payloads.response;
import lombok.AllArgsConstructor;
import lombok.Data;
@Data
@AllArgsConstructor
public class TaskResponse {
private String message;
}
- The above code is simply a POJO (Plain Old Java Object) with one instance variable, a constructor, a mutator(setters), and an accessor(getters).
The Repository
Inside the data package, create a sub-package called a repository. Then create an interface called “TaskRepository” that extends JpaRepository. The JpaRepository is generic so it takes a model class(Type) and the data type of the primary key. Write the following code in the TaskRepository interface.
package com.taskVentures.taskmanager.data.repository;
import com.taskVentures.taskmanager.data.models.Task;
import org.springframework.stereotype.Repository;
import java.util.List;
import java.util.concurrent.CompletableFuture;
@Repository
public class TaskRepository extends FaunaRepository<Task> {
public TaskRepository(){
super(Task.class, "todos", "tasks");
}
@Override
public CompletableFuture<List<Task>> findAll() {
return null;
}
}
-
@Repository makes the interface a bean. It is treated identically to the @Component annotation, therefore it is a specialization of the @Component annotation.
Beans
are simply Java classes that spring knows.
Next let’s create a class called FaunaRepository that will contain methods that will allow us to perform the CRUD Operation. We are going to first create an interface that will contain these methods. Let's call the interface Repository
.
package com.taskVentures.taskmanager.data.repository;
import com.taskVentures.taskmanager.data.models.Task;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
public interface Repository<T extends Task> {
CompletableFuture<T> save(T entity);
CompletableFuture<Optional<T>> find(String id);
CompletableFuture<Optional<T>> remove(String id);
}
- We have defined an interface with methods that allow us to save, find, and update a task. ```Java
package com.taskVentures.taskmanager.data.repository;
import com.faunadb.client.FaunaClient;
import com.faunadb.client.errors.NotFoundException;
import com.faunadb.client.query.Expr;
import com.faunadb.client.query.Language;
import com.faunadb.client.types.Value;
import com.taskVentures.taskmanager.data.models.Task;
import org.springframework.beans.factory.annotation.Autowired;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
import java.util.function.Function;
import static com.faunadb.client.query.Language.*;
import java.lang.Class;
public abstract class FaunaRepository implements Repository, IdentityFactory {
@Autowired
protected FaunaClient faunaClient;
protected final Class<T> entityType;
protected final String collectionName;
protected final String collectionIndexName;
protected FaunaRepository(Class<T> entityType, String collectionName, String collectionIndexName) {
this.entityType = entityType;
this.collectionName = collectionName;
this.collectionIndexName = collectionIndexName;
}
@Override
public CompletableFuture<String> nextId() {
CompletableFuture<String> result =
faunaClient.query(
NewId()
)
.thenApply(value -> value.to(String.class).get());
return result;
}
@Override
public CompletableFuture<T> save(T entity) {
CompletableFuture<T> result =
faunaClient.query(
saveQuery(Language.Value(entity.getId()), Value(entity))
)
.thenApply(this::toEntity);
return result;
}
@Override
public CompletableFuture<Optional<T>> remove(String id) {
CompletableFuture<T> result =
faunaClient.query(
Select(
Value("data"),
Delete(Ref(Collection(collectionName), Value(id)))
)
)
.thenApply(this::toEntity);
CompletableFuture<Optional<T>> optionalResult = toOptionalResult(result);
return optionalResult;
}
@Override
public CompletableFuture<Optional<T>> find(String id) {
CompletableFuture<T> result =
faunaClient.query(
Select(
Value("data"),
Get(Ref(Collection(collectionName), Value(id)))
)
)
.thenApply(this::toEntity);
CompletableFuture<Optional<T>> optionalResult = toOptionalResult(result);
return optionalResult;
}
protected Expr saveQuery(Expr id, Expr data) {
Expr query =
Select(
Value("data"),
If(
Exists(Ref(Collection(collectionName), id)),
Replace(Ref(Collection(collectionName), id), Obj("data", data)),
Create(Ref(Collection(collectionName), id), Obj("data", data))
)
);
return query;
}
protected T toEntity(Value value) {
return value.to(entityType).get();
}
protected CompletableFuture<Optional<T>> toOptionalResult(CompletableFuture<T> result) {
CompletableFuture<Optional<T>> optionalResult =
result.handle((v, t) -> {
CompletableFuture<Optional<T>> r = new CompletableFuture<>();
if(v != null) r.complete(Optional.of(v));
else if(t != null && t.getCause() instanceof NotFoundException) r.complete(Optional.empty());
else r.completeExceptionally(t);
return r;
}).thenCompose(Function.identity());
return optionalResult;
}
}
The above class provides an implementation to the methods defined on the interface.
You can look up the Fauna documentation for Java by clicking on this link: [Fauna/JVM doc](https://docs.fauna.com/fauna/current/drivers/jvm)
```Java
package com.taskVentures.taskmanager.data.repository;
import java.util.concurrent.CompletableFuture;
public interface IdentityFactory {
CompletableFuture<String> nextId();
}
The TaskService
Create a service package under the taskmanager directory. This package is going to house the business logic. We have divided the service into two, an interface where the methods of our business logic will be declared and a concrete class that implements the interface. Create an interface with the name “taskService" with the following code:
package com.taskVentures.taskmanager.services;
import com.taskVentures.taskmanager.data.models.Task;
import com.taskVentures.taskmanager.data.payloads.requests.TaskRequest;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
@component
public interface TaskService {
CompletableFuture<Task> createTask(TaskRequest taskRequest);
CompletableFuture<Optional<Task>> updateTask(String id, TaskRequest taskRequest);
CompletableFuture<Optional<Task>> deleteTask(String id);
CompletableFuture<Optional<Task>> getTask(String id);
}
-
@Component annotation is a shorthand for the
@Bean
annotation. It registers the TaskService interface as a bean in the application context and makes it accessible during classpath scanning. We created five methods that allow us to create, update, get and delete tasks.
Next, create a TaskServiceImpl class that implements the TaskService interface. Write the following code:
package com.taskVentures.taskmanager.services;
import com.taskVentures.taskmanager.data.models.Task;
import com.taskVentures.taskmanager.data.payloads.requests.TaskRequest;
import com.taskVentures.taskmanager.data.repository.TaskRepository;
import lombok.AllArgsConstructor;
import org.springframework.stereotype.Service;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
@Service
@AllArgsConstructor
public class TaskServiceImpl implements TaskService {
private final TaskRepository taskRepository;
@Override
public CompletableFuture<Task> createTask(TaskRequest taskRequest) {
CompletableFuture<Task> newTask = taskRepository.nextId()
.thenApply(id -> new Task(id, taskRequest.getName(), taskRequest.getDescription(), taskRequest.isCompleted())).thenCompose(taskRepository::save);
return newTask;
}
@Override
public CompletableFuture<Optional<Task>> getTask(String id) {
return taskRepository.find(id);
}
@Override
public CompletableFuture<Optional<Task>> updateTask(String id, TaskRequest taskRequest) {
CompletableFuture<Optional<Task>> result =
taskRepository.find(id)
.thenCompose(optionalTodoEntity ->
optionalTodoEntity
.map(todoEntity -> taskRepository.save(new Task(id, taskRequest.getName(), taskRequest.getDescription(), taskRequest.isCompleted())).thenApply(Optional::of))
.orElseGet(() -> CompletableFuture.completedFuture(Optional.empty())));
return result;
}
@Override
public CompletableFuture<Optional<Task>> deleteTask(String id) {
return taskRepository.remove(id);
}
}
@Service annotation is a specialized form of @Component
. With the @Service annotation, the class that is annotated is registered in the application context and accessible during classpath scanning.
The TaskServiceImpl class implemented the TaskService interface by overriding the method and implementing them.
The class throws an exception(ResourceNotFoundException- This is the custom exception class we created that extends RunTimeException) where the Id supplied to get a single task does not exist on the database.
The Controller
Create a package called web under the taskmanager package. This package is going to house the APIs controller. Create an TaskController class with the following code:
package com.taskVentures.taskmanager.web;
import com.taskVentures.taskmanager.data.payloads.requests.TaskRequest;
import com.taskVentures.taskmanager.services.TaskService;
import io.swagger.annotations.ApiResponses;
import lombok.AllArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.concurrent.CompletableFuture;
@RestController
@RequestMapping("/task")
@AllArgsConstructor
@ApiResponses(value = {
@io.swagger.annotations.ApiResponse(code = 400, message = "This is a bad request, please follow the API documentation for the proper request format"),
@io.swagger.annotations.ApiResponse(code = 401, message = "Due to security constraints, your access request cannot be authorized"),
@io.swagger.annotations.ApiResponse(code = 500, message = "The server is down. Please bear with us."),
})
public class TaskController {
TaskService taskService;
@PostMapping("/create")
public CompletableFuture<?> createTask(@RequestBody TaskRequest taskRequest) {
return taskService.createTask(taskRequest)
.thenApply(todoEntity -> new ResponseEntity<>(todoEntity, HttpStatus.CREATED));
}
@GetMapping("/get/{id}")
public CompletableFuture<?> getTask(@PathVariable("id") String id) {
CompletableFuture<ResponseEntity> result =
taskService.getTask(id)
.thenApply(optionalTodoEntity ->
optionalTodoEntity
.map(todoEntity -> new ResponseEntity<>(todoEntity, HttpStatus.OK))
.orElseGet(() -> new ResponseEntity<>(HttpStatus.NOT_FOUND))
);
return result;
}
@PutMapping("/update/{id}")
public CompletableFuture<?> updateTask(@PathVariable("id") String id, @RequestBody TaskRequest taskRequest) {
CompletableFuture<ResponseEntity> result =
taskService.updateTask(id, taskRequest)
.thenApply(optionalTodoEntity ->
optionalTodoEntity
.map(todoEntity -> new ResponseEntity<>(todoEntity, HttpStatus.OK))
.orElseGet(() -> new ResponseEntity<>(HttpStatus.NOT_FOUND)
)
);
return result;
}
@DeleteMapping(value = "/delete/{id}")
public CompletableFuture<?> deleteTask(@PathVariable("id")String id) {
CompletableFuture<ResponseEntity> result =
taskService.deleteTask(id)
.thenApply(optionalTodoEntity ->
optionalTodoEntity
.map(todo -> new ResponseEntity<>(todo, HttpStatus.OK))
.orElseGet(() -> new ResponseEntity<>(HttpStatus.NOT_FOUND)
)
);
return result;
}
}
- @RestController: This annotation marks the EmployeeController as an HTTP request handler and allows Spring to recognize it as a RESTful service.
- @RequestMapping("/task") annotation sets the base path to the resource endpoints in the controller as /task. Next, we injected the TaskService class.
- @GetMapping is a shortcut for @RequestMapping(method = RequestMethod.GET), and is used to map HTTP GET requests to the mapped controller methods. We used it to return all the tasks and a single task.
- @PathVariable annotation shows that a method parameter should be bound to a URI template variable.
- @PostMapping is a shorthand for @RequestMapping where the method is equal to POST. It is used to map HTTP POST requests to the mapped controller methods.
- @RequestBody: This annotation takes care of binding the web request body to the method parameter with the help of the registered HttpMessageConverters. So when you make a POST request to the “/task/add” URL with a Post JSON body, the HttpMessageConverters converts the JSON request body into a Post object and passes it to the createTask method.
- @PutMapping is a shorthand for @RequestMapping where the method is equal to PUT. It is used to map HTTP PUT requests to the mapped controller methods.
- @DeleteMapping: Using this annotation makes the Mapped controller method to be ready for a delete operation. is a shortcut for @RequestMapping (method = RequestMethod.DELETE).
Documenting your API with Swagger
We already added the io.springfox dependency to the pom.xml. With this dependency we will document the API so that it will be easy for other developers to use it. All is required is to add the following line of code at the class level of our controller as follows:
@ApiResponses(value = {
@io.swagger.annotations.ApiResponse(code = 400, message = "This is a bad request, please follow the API documentation for the proper request format"),
@io.swagger.annotations.ApiResponse(code = 401, message = "Due to security constraints, your access request cannot be authorized"),
@io.swagger.annotations.ApiResponse(code = 500, message = "The server is down. Please bear with us."),
})
We added the @ApiResponse annotation from swagger at the class level. As simple as this, our APIs are fully documented.
Go to localhost:8081/swagger-ui to access the documentation and test that our APIs are still working properly.
Use the Swagger API document at localhost:8900/swagger-ui to add an employee, get, update and delete an employee.
Conclusion
In this article project, we successfully built a Task Management Application using SpringBoot
framework and Maven
as our dependency management and build tools. We used Fauna
as our Cloud datastore.
Additionally, we learned how to throw exceptions in our application which ensures that our application is fault-tolerant and resilient. We also learned how to document our API using Swagger
. You can clone the project from my GitHub via this link: Task_Management_SpringBoot_Project
If you have any questions, don’t hesitate to contact me via any of my socials:
Posted on November 30, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.