The image doesn’t relate to the content. 😅
Disclaimer
I’m not a Java Developer.
So, this blog post is just a record of my playing around in Spring Boot and MongoDB + Docker.
Therefore, that means I can follow these steps again later.
So, sorry for most of the parts in this blog post, I can’t explain so much detail.
If I have some time, I will explain about them in the next blog posts.
If you have any questions, don’t hesitate to ask.
And if I understand something wrong, please let me know. 😅
You can feel free to fork or clone this repository.
Prerequisite
- Your favorite IDE (I prefer the IntelliJ)
- Docker (and Docker Compose)
- Java (JDK) version 17 or more on your local machine (I use Java 21)
- Gradle 8
Project Setting & Dependencies
- Project: Gradle — Kotlin
- Language: Java
- Spring Boot Version: 3.x.x (3.0.0 ≤ your_version < 4.0.0)
- Packaging: Jar
- Java: 17
- Dependency: Spring Reactive Web
- Dependency: Lombok
- Dependency: Spring Data Reactive MongoDB
- Dependency: Spring Boot DevTools
- Dependency: Docker Compose Support
Note:
For Spring Boot version 4 or more may not support for the code in this blog post.
Or download this initial project configuration.
Step-by-step
Start the project in Spring Initializr above, then open the project directory by your favorite IDE.
When you open the build.gradle.kts
file. You should see it like this.
plugins {
java
id("org.springframework.boot") version "3.1.5"
id("io.spring.dependency-management") version "1.1.3"
}
group = "dev.fresult"
version = "0.0.1"
java {
sourceCompatibility = JavaVersion.VERSION_17
}
configurations {
compileOnly {
extendsFrom(configurations.annotationProcessor.get())
}
}
repositories {
mavenCentral()
}
dependencies {
implementation("org.springframework.boot:spring-boot-starter-data-mongodb-reactive")
implementation("org.springframework.boot:spring-boot-starter-webflux")
compileOnly("org.projectlombok:lombok")
developmentOnly("org.springframework.boot:spring-boot-devtools")
annotationProcessor("org.projectlombok:lombok")
testImplementation("org.springframework.boot:spring-boot-starter-test")
testImplementation("io.projectreactor:reactor-test")
}
tasks.withType<Test> {
useJUnitPlatform()
}
And then follow these steps
1. Start with the Docker Compose file
Create the compose.dev.yml
file in the docker
directory then open it.
mkdir docker
# THEN
touch docker/compose.dev.yml
# THEN
open docker/compose.dev.yml
File: /docker/compose.dev.yml
version: "3.8"
services:
mongodb:
image: mongo:latest
container_name: webflux-mongodb
restart: unless-stopped
environment:
- "MONGO_INITDB_DATABASE=my-db"
- "MONGO_INITDB_ROOT_USERNAME=admin"
- "MONGO_INITDB_ROOT_PASSWORD=my-password"
ports:
- "27777:27017"
volumes:
- mongodb:/data/db
volumes:
mongodb:
driver: local
2. Create a containerized database
Run this script in your terminal
docker-compose -f docker/compose.dev.yml up -d --build
The terminal should display like this
[+] Running 1/1
✔ mongodb Pulled 2.8s
[+] Building 0.0s (0/0) docker:desktop-linux
[+] Running 1/0
✔ Container webflux-mongodb Running
3. Add the application configuration
Change the file application.properties
in the /src/main/resources
directory to be application.yml
.
Then copy this config below.
File: /src/main/resources/application.yml
server:
port: 8081
spring:
data:
mongodb:
host: localhost
port: 27777
database: ${MONGO_INITDB_ROOT_PASSWORD:my-db}
username: ${MONGO_INITDB_ROOT_USERNAME}
password: ${MONGO_INITDB_ROOT_PASSWORD}
authentication-database: admin
docker:
compose:
file: ./docker/compose.dev.yml
enabled: true
4. Create the entity (Document) class and repository interface
Car.java
File: /src/main/java/dev/fresult/springreactivemongo/documents/Car.javatouch
@Data
@Document
@RequiredArgsConstructor
public class Car {
private @Id String id = null;
private @NonNull String brand;
private @NonNull String model;
}
CarRepository.java
File: /src/main/java/dev/fresult/springreactivemongo/repositories/CarRepository.java
@Repository
public interface CarRepository extends ReactiveMongoRepository<Car, String> {
Flux<Car> findByBrandIgnoreCase(String brand);
Flux<Car> findByModelIgnoreCase(String model);
}
5. Create the service class
CarService.java
File: /src/main/java/dev/fresult/springreactivemongo/services/CarService.java
@Service
@RequiredArgsConstructor
public class CarService {
private final CarRepository repo;
public Flux<Car> all() {
return repo.findAll();
}
public Mono<Car> byId(String id) {
return repo.findById(id);
}
public Flux<Car> byBrand(String brand) {
return repo.findByBrandIgnoreCase(brand);
}
public Mono<Car> create(Car car) {
return repo.save(car);
}
public Mono<Car> update(String id, Car car) {
return createCarMonoOpt(repo.findById(id))
.flatMap(carOpt -> {
if (carOpt.isEmpty()) return Mono.empty();
car.setId(id);
return repo.save(car);
});
}
public Mono<Void> deleteById(String id) {
return repo.deleteById(id);
}
public Mono<Optional<Car>> createCarMonoOpt(Mono<Car> carMono) {
return carMono.map(Optional::of).defaultIfEmpty(Optional.empty());
}
}
6. Create the controller class
CarController.java
File: /src/main/java/dev/fresult/springreactivemongo/controllers/CarController.java
@RestController
@RequestMapping("/cars")
@RequiredArgsConstructor
public class CarController {
private final CarService service;
@GetMapping
public Flux<Car> all(@RequestParam(required = false, value = "brand") Optional<String> brandOpt) {
if (brandOpt.isEmpty()) return service.all();
return service.byBrand(brandOpt.get());
}
@GetMapping("/{id}")
public Mono<ResponseEntity<?>> byId(@PathVariable String id) {
return service.createCarMonoOpt(service.byId(id)).flatMap(flatMapMonoCarOK);
}
@PostMapping
public Mono<ResponseEntity<?>> create(@RequestBody Car car, UriComponentsBuilder uriBuilder) {
try {
return service.create(car).flatMap(createdCar -> {
String location = uriBuilder.path("cars/{id}").buildAndExpand(createdCar.getId()).toUriString();
return Mono.just(ResponseEntity.created(URI.create(location)).body(createdCar));
});
} catch (DecoderException e) {
System.err.println("error: " + e.getMessage());
return Mono.just(ResponseEntity.badRequest().body(e.getCause()));
} catch (Exception e) {
System.err.println(e.getMessage());
return Mono.just(ResponseEntity.internalServerError().body(e.getCause()));
}
}
@PutMapping("/{id}")
public Mono<ResponseEntity<?>> update(@PathVariable String id, @RequestBody Car car) {
try {
return service.createCarMonoOpt(service.update(id, car))
.flatMap(flatMapMonoCarOK);
} catch (DecoderException e) {
System.err.println("error: " + e.getMessage());
return Mono.just(ResponseEntity.badRequest().body(e.getCause()));
} catch (Exception e) {
System.err.println(e.getMessage());
return Mono.just(ResponseEntity.internalServerError().body(e.getCause()));
}
}
@DeleteMapping("/{id}")
public ResponseEntity<Void> deleteById(@PathVariable String id) {
service.deleteById(id).subscribe();
return ResponseEntity.noContent().build();
}
private final Function<Optional<Car>, Mono<ResponseEntity<?>>> flatMapMonoCarOK = carOpt ->
carOpt.<Mono<ResponseEntity<?>>>map(car -> Mono.just(ResponseEntity.ok(car)))
.orElseGet(() -> Mono.just(ResponseEntity.notFound().build()));
}
6. Prepare API Collection for Postman
Copy this JSON and import it to Postman
{
"collection": {
"info": {
"_postman_id": "b916cc2f-f7fc-47cd-a96b-c7e20abe42eb",
"name": "WebReactiveMongo",
"schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json",
"updatedAt": "2023-10-27T07:32:53.000Z",
"uid": "29603661-b916cc2f-f7fc-47cd-a96b-c7e20abe42eb"
},
"item": [
{
"name": "All Cars",
"id": "7be4cd32-13da-43df-8119-899041b809a0",
"protocolProfileBehavior": {
"disableBodyPruning": true
},
"request": {
"method": "GET",
"header": [],
"url": {
"raw": "{{base-url}}/cars?brand=honda",
"host": [
"{{base-url}}"
],
"path": [
"cars"
],
"query": [
{
"key": "brand",
"value": "honda"
}
]
}
},
"response": [],
"uid": "29603661-7be4cd32-13da-43df-8119-899041b809a0"
},
{
"name": "Car By Id",
"id": "3466933f-9aaa-4933-9358-405f6e07c184",
"protocolProfileBehavior": {
"disableBodyPruning": true
},
"request": {
"method": "GET",
"header": [],
"url": {
"raw": "{{base-url}}/cars/:id",
"host": [
"{{base-url}}"
],
"path": [
"cars",
":id"
],
"variable": [
{
"key": "id",
"value": "653b50f810597d26ada6624f"
}
]
}
},
"response": [],
"uid": "29603661-3466933f-9aaa-4933-9358-405f6e07c184"
},
{
"name": "Create",
"id": "0fc21395-a2b4-469f-a2b5-e072dbab98cd",
"protocolProfileBehavior": {
"disableBodyPruning": true
},
"request": {
"method": "POST",
"header": [],
"body": {
"mode": "raw",
"raw": "{\n \"brand\": \"Honda\",\n \"model\": \"Civic\"\n}",
"options": {
"raw": {
"language": "json"
}
}
},
"url": {
"raw": "{{base-url}}/cars",
"host": [
"{{base-url}}"
],
"path": [
"cars"
]
}
},
"response": [],
"uid": "29603661-0fc21395-a2b4-469f-a2b5-e072dbab98cd"
},
{
"name": "Update Car by Id",
"id": "5a5350b8-0062-4bd9-9fad-dfbc7dbd2dcc",
"protocolProfileBehavior": {
"disableBodyPruning": true
},
"request": {
"method": "PUT",
"header": [],
"body": {
"mode": "raw",
"raw": "{\n \"brand\": \"Honda\",\n \"model\": \"Civic\"\n}",
"options": {
"raw": {
"language": "json"
}
}
},
"url": {
"raw": "{{base-url}}/cars/:id",
"host": [
"{{base-url}}"
],
"path": [
"cars",
":id"
],
"variable": [
{
"key": "id",
"value": "653b50f810597d26ada6624f"
}
]
}
},
"response": [],
"uid": "29603661-5a5350b8-0062-4bd9-9fad-dfbc7dbd2dcc"
},
{
"name": "Delete By Id",
"id": "f12a6c83-5ba2-45b0-acb6-10d684c6ca20",
"protocolProfileBehavior": {
"disableBodyPruning": true
},
"request": {
"method": "DELETE",
"header": [],
"url": {
"raw": "{{base-url}}/cars/:id",
"host": [
"{{base-url}}"
],
"path": [
"cars",
":id"
],
"variable": [
{
"key": "id",
"value": "653b6739c05d36108e550f6f",
"type": "string"
}
]
}
},
"response": [],
"uid": "29603661-f12a6c83-5ba2-45b0-acb6-10d684c6ca20"
}
]
}
}
Then prepare the Environment Variable in your Postman.
Set base-url
as http://localhost:8081
as below.
7. Test APIs
1. Create 3 cars
2. Get all cars
3. Get cars by brand honda
4. Get the car by ID
5. Update the car from Honda City to be Honda Civic.
6. Then get all cars again
7. Delete car (Honda Civic)
We will get a response: 204 No content.
8. Get all cars again
The Honda Civic is already gone.
My solution repository.
Here is a series of Reactive Programming with Reactor & Spring Boot Webflux (The Road to Backend Developer Edition).
Check it out and follow me to get an update about it.
Thanks for reading until the final line.
If you’re confused or have any questions, don’t hesitate to leave the comments.
Feel free to connect with me and stay updated on the latest insights and discussions.
👉 https://linkedin.com/in/fResult 👈