Google Cloud Firestore + Spring Boot + Spring WebFlux - Build Reactive CRUD REST APIs

In this section, we will learn how to build reactive REST CRUD APIs with Spring Boot, Spring WebFlux, and Google Cloud Datastore.


1.  A little bit of Background

Reactive APIs

Reactive APIs are non-blocking and tend to be more efficient because they’re not tying up processing while waiting for stuff to happen. Reactive systems adopt asynchronous I/O. Reactive apps allow us to scale better if we are dealing with lots of streaming data. If we are going to build a reactive app, we need it to be reactive all the way down to your database. 

Google Cloud Firestore

Firestore is a NoSQL document database built for automatic scaling, high performance, and ease of application development. While the Firestore interface has many of the same features as traditional databases, as a NoSQL database it differs from them in the way it describes relationships between data objects. 

Spring WebFlux

The reactive-stack web framework, Spring WebFlux, was added later in version 5.0. It is fully non-blocking, supports Reactive Streams back pressure, and runs on such servers as Netty, Undertow, and Servlet containers.

Spring Boot

Spring Boot makes it easy to create stand-alone, production-grade Spring-based Applications that you can "just run". 


2. Create a GCP Project

First, Sign into the Google console at https://console.cloud.google.com.

You can create a new project by first selecting the project dropdown in the top left and selecting "New Project".


Next, specify your GCP Project name and Project ID.

Then Click on the "CREATE" button.

Copy "Project ID" and keep it for future purposes.


3. Create a Firestore in Native mode database

From cloud console, search for "Firestore" like below and click on "Firestore" button.


Next, click on "SELECT NATIVE MODE" button,


After that, you will see "Choose where to store your data" screen. Select nam5 or any other regional location and click "CREATE DATABASE".


4. Create a service account key

First choose "IAM & Admin" and then click on "Service accounts".

After that, click on "CREATE SERVICE ACCOUNT".

Then, enter service account details like "Service account name", and "Service account ID" and click on "CREATE AND CONTINUE".

Then, grant basic role Editor.

Finally click on "DONE" button.

Then, from "Actions" click on "Manage keys".

Then, click on "Create new key".

Then, choose "Key type" as JSON and click on "CREATE" button.

Service account keys in JSON format will be download. Keep the file safe for future purposes.


5. Creating a simple spring boot web application

First, open the Spring initializr https://start.spring.io/ 

Then, Provide the Group and Artifact name. We have provided Group name com.knf.dev.demo and Artifact spring-boot-google-firestore-crud. Here I selected the Maven project - language Java 17 - Spring Boot 3.0.3 and add Spring Reactive web dependency and GCP Support.

Then, click on the Generate button. When we click on the Generate button, it starts packing the project in a .zip(spring-boot-google-firestore-crud) file and downloads the project. Then, Extract the Zip file. 

Then, import the project on your favourite IDE.

Final Project directory:

Note: Place service account key JSON file in resources folder (Not recommended in production environment).


In the pom.xml, add the Spring Data Cloud Firestore Spring Boot starter dependency:

<!-- Add GCP Firestore Starter -->
<dependency>
<groupId>com.google.cloud</groupId>
<artifactId>spring-cloud-gcp-starter-data-firestore</artifactId>
</dependency>


Complete pom.xml

<?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>3.0.3</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.knf.dev.demo</groupId>
<artifactId>spring-boot-google-firestore-crud</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>spring-boot-google-firestore-crud</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>17</java.version>
<spring-cloud-gcp.version>4.1.0</spring-cloud-gcp.version>
<spring-cloud.version>2022.0.1</spring-cloud.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
<dependency>
<groupId>com.google.cloud</groupId>
<artifactId>spring-cloud-gcp-starter</artifactId>
</dependency>
<!-- Add GCP Firestore Starter -->
<dependency>
<groupId>com.google.cloud</groupId>
<artifactId>spring-cloud-gcp-starter-data-firestore</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.projectreactor</groupId>
<artifactId>reactor-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>com.google.cloud</groupId>
<artifactId>spring-cloud-gcp-dependencies</artifactId>
<version>${spring-cloud-gcp.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>

<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>

</project>


application.properties

Specify datastore project id, and credential's location to application.properties file.

spring.cloud.gcp.firestore.project-id=knf-gcp-demo-project
spring.cloud.gcp.credentials.location=classpath:knf-gcp-demo-project-2e3b862b8c62.json
Note: In this example, we placed service account key JSON file in resources folder (Not recommended in production environment).

More secure way is place JSON in somewhere in server or docker image, then create environment variable "GOOGLE_APPLICATION_CREDENTIALS" and give the location to your JSON FILE. 

If your application is running on Google App Engine or Google Compute Engine, in most cases you should omit the "spring.cloud.gcp.credentials.location" property and instead, let Spring Cloud GCP Core Starter find the correct credentials for those environments.

Create Document User

package com.knf.dev.demo.document;

import com.google.cloud.firestore.annotation.DocumentId;
import com.google.cloud.spring.data.firestore.Document;

@Document(collectionName = "users")
public class User {

@DocumentId
String id;
String name;
String email;
String country;

public String getId() {
return id;
}

public void setId(String id) {
this.id = id;
}

public String getName() {
return name;
}

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

public String getEmail() {
return email;
}

public void setEmail(String email) {
this.email = email;
}

public String getCountry() {
return country;
}

public void setCountry(String country) {
this.country = country;
}

public User(String name, String email, String country) {
this.name = name;
this.email = email;
this.country = country;
}

public User() {
}
}

  • @Document: Annotation for a class that represents a Firestore Document. 
  • @DocumentId: Annotation used to mark a POJO property to be automatically populated with the document's ID when the POJO is created from a Cloud Firestore document.


Create User Repository

package com.knf.dev.demo.repository;

import com.google.cloud.spring.data.firestore.FirestoreReactiveRepository;
import com.knf.dev.demo.document.User;
import reactor.core.publisher.Flux;

public interface UserRepository extends FirestoreReactiveRepository<User> {

Flux<User> findByCountry(String country);

}

Spring Data Repositories is an abstraction that can reduce boilerplate code.


Create User Service

package com.knf.dev.demo.service;

import com.knf.dev.demo.document.User;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

public interface UserService {

Mono<User> save(User user);

Mono<User> delete(String id);

Mono<User> update(String id, User user);

Flux<User> findAll();

Mono<User> findById(String id);

Flux<User> findByCountry(String country);
}


Create UserServiceImpl

package com.knf.dev.demo.service;

import com.knf.dev.demo.document.User;
import com.knf.dev.demo.repository.UserRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

@Service
public class UserServiceImpl implements UserService{

@Autowired
UserRepository userRepository;

@Override
public Mono<User> save(User user) {

return userRepository.save(user);
}

@Override
public Mono<User> delete(String id) {

return this.userRepository
.findById(id).flatMap(p ->
this.userRepository
.deleteById(p.getId())
.thenReturn(p));
}

@Override
public Mono<User> update(String id, User user) {

return this.userRepository.findById(id)
.flatMap(u -> {
u.setId(id);
u.setEmail(user.getEmail());
u.setName(user.getName());
u.setCountry(user.getCountry());
return save(u);
}).switchIfEmpty(Mono.empty());
}

@Override
public Flux<User> findAll() {
return userRepository.findAll();
}

@Override
public Mono<User> findById(String id) {
return userRepository.findById(id);
}

@Override
public Flux<User> findByCountry(String country) {
return userRepository.findByCountry(country);
}
}

  • @Service annotation serves as a specialization of @Component, allowing for implementation classes to be autodetected through classpath scanning.
  • We can use the @Autowired to mark a dependency which Spring is going to resolve and inject.

  • Mono is used for handling zero or one result, the Flux is used to handle zero to many results, possibly even infinite results.


Create User Controller

package com.knf.dev.demo.controller;

import com.knf.dev.demo.document.User;
import com.knf.dev.demo.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

@RestController
@RequestMapping("/api/v1")
public class UserController {

@Autowired
private UserService userService;

@PostMapping("/users")
@ResponseStatus(HttpStatus.CREATED)
private Mono<User> save(@RequestBody User user) {

return userService.save(user);
}

@DeleteMapping("/users/{id}")
private Mono<ResponseEntity<String>> delete(
@PathVariable String id) {

return userService.delete(id)
.flatMap(user -> Mono.just(ResponseEntity
.ok("Deleted Successfully")))
.switchIfEmpty(Mono.just(ResponseEntity
.notFound().build()));

}

@PutMapping("/users/{id}")
private Mono<ResponseEntity<User>> update(
@PathVariable String id,
@RequestBody User user) {

return userService.update(id, user)
.flatMap(user1 -> Mono.just(ResponseEntity
.ok(user1))).switchIfEmpty(Mono
.just(ResponseEntity.notFound().build()));

}

@GetMapping(value = "/users")
private Flux<User> findAll() {

return userService.findAll();
}

@GetMapping(value = "/users/{id}")
private Mono<User> findUserById( @PathVariable String id) {

return userService.findById(id);
}

@GetMapping(value = "/users/country/{name}")
private Flux<User> findAllUsersByCountry(
@PathVariable String name) {

return userService.findByCountry(name);
}
}

  • Spring @RestController annotation is used to create RESTful web services using Spring MVC. Spring RestController takes care of mapping request data to the defined request handler method. Once response body is generated from the handler method, it converts it to JSON response.
  • @RequestMapping is used to map web requests onto specific handler classes and/or handler methods. @RequestMapping can be applied to the controller class as well as methods.
  • We can use the @Autowired to mark a dependency which Spring is going to resolve and inject.
  • @GetMapping annotation for mapping HTTP GET requests onto specific handler methods.
  • @PostMapping annotation for mapping HTTP POST requests onto specific handler methods.
  • @PutMapping annotation for mapping HTTP PUT requests onto specific handler methods.
  • @DeleteMapping annotation for mapping HTTP DELETE requests onto specific handler methods.
  • @RequestBody annotation is used to indicating a method parameter should be bind to the body of the HTTP request. Internally, this annotation uses HTTP Message converters to convert the body of HTTP requests to domain objects.
  • @PathVariable annotation used on a method argument to bind it to the value of a URI template variable.


Run the application - Application.java

package com.knf.dev.demo;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class Application {

public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}

}

Application is the entry point that sets up the Spring Boot application. The @SpringBootApplication annotation enables auto-configuration and component scanning. 

Step1: Download or clone the source code from GitHub to a local machine - Click here!


Step 2: mvn clean install


Step 3: Run the Spring Boot application - mvn spring-boot:run


OR 


Run this Spring boot application from 

  • IntelliJ IDEA IDE by right click - Run 'Application.main()' 
  • Eclipse/STS - You can right click the project or the Application.java file and run as java application or Spring boot application.

Add User:


Update User:


Find all Users:


Find User by Id:


Delete User:


Find Users by country:


Source code - click here!

Next section: Deploy a Spring Boot application on Google Cloud App Engine - Click here!

Popular posts from this blog

Learn Java 8 streams with an example - print odd/even numbers from Array and List

Java Stream API - How to convert List of objects to another List of objects using Java streams?

Registration and Login with Spring Boot + Spring Security + Thymeleaf

Java, Spring Boot Mini Project - Library Management System - Download

ReactJS, Spring Boot JWT Authentication Example

Spring Boot + Mockito simple application with 100% code coverage

Top 5 Java ORM tools - 2024

Java - Blowfish Encryption and decryption Example

Spring boot video streaming example-HTML5

Google Cloud Storage + Spring Boot - File Upload, Download, and Delete