Matthew Tyson
Contributing writer

Full-stack development with Java, React, and Spring Boot, Part 2

how-to
Jul 31, 20248 mins
DatabasesJavaReact

Explore the popular and powerful React + Java + Spring stack by incorporating a service layer and a MongoDB instance for data persistence.

Reminder, to-do, calendar. The words "don't forget" on a sticky note.
Credit: Cagkan Sayin / Shutterstock

In the first article in this series, we built a simple Todo app that showed how to combine a React front end with a Java back end using Spring Boot. We also looked at defining JSON API endpoints on the server and interacting with them using React components and JavaScript’s Fetch API. So far, we’ve just saved the to-do items in memory on the server. Now, we’ll build out a more realistic back end that uses MongoDB and Spring Data for persistence.

Developing the data layer with MongoDB and Spring Data

Adding a real database layer is the next step in the evolution of our application. For that, we’ll use a local MongoDB instance installed as a service. Running a local MongoDB instance is fairly easy on the operating system, and is described in the MongoDB docs. For this demo, I installed my local database without any admin credentials, which we’d never do in real life.

To connect to MongoDB, we need a couple of dependencies in our pom.xml: the MongoDB driver and the Spring Data MongoDB library. Add these to the POM like I’ve done here:


<dependency>
   <groupId>org.mongodb</groupId>
   <artifactId>mongodb-driver-sync</artifactId>
   <version>4.11.2</version>
 </dependency>

<dependency>
   <groupId>org.springframework.data</groupId>
   <artifactId>spring-data-mongodb</artifactId>
   <version>4.2.5</version>
</dependency>

The MongoDB driver lets us connect to a database instance, and the Spring Data library lets us use a MongoDB repository for a quick and easy data layer.

Add persistence to the model class

We already have a model class called TodoItem, which holds the data on a to-do item. Now we can add a couple of annotations from Spring Data to make it persistent. Note that the rest of the class remains the same, just getters and setters:


package com.example.iwreactspring.model;

import org.springframework.data.mongodb.core.mapping.Document;
import org.springframework.data.annotation.Id;

@Document(collection = "todo") // Optional: Specify the collection name
public class TodoItem {

  @Id
  private String id;
//...

The @Document annotation tells Spring we have a MongoDB document, and the optional collection attribute tells it what collection name to use. We use the @Id field to define what the id field will be. By default, MongoDB generates a random UUID on the _id field, which will work fine for us.

This is all we need for a persistent model class.

The repository class

Now we need a mechanism for moving to-dos in and out of the database. Spring gives us the concept of a Repository class that makes basic CRUD operations very easy to do. It also supports more sophisticated operations like querying and filtering. For now, we just need the basics, keeping the class short and sweet:


//src/main/java/com/example/iwreactspring/repository/TodoRepository.java 
package com.example.iwreactspring.repository;

import org.springframework.data.mongodb.repository.MongoRepository;
import com.example.iwreactspring.model.TodoItem;
import org.springframework.stereotype.Repository;

@Repository
public interface TodoRepository extends MongoRepository<TodoItem, String> { }

That it! The key to tying this to our model class is parameterizing the generics on the class with <TodoItem, String>. The String ID says: This is a MongoRepository class dedicated to the TodoItem class. (The @Repository annotation is considered good practice, but is not strictly necessary.)

The service layer

Previously, our controller exposed the APIs the front end needed to make calls to, and it implemented the business logic right there. But now we’re going to have more elaborate layers, something like this:


Controller <-> Service <-> Repository

Generally, these components work together to isolate the logic into concentrated layers:

  • Controller: The logic for exposing the API and translating between the back end and front end.
  • Service: The business logic or “middleware” that does most of the custom heavy lifting based on the information from the controller and repository.
  • Repository: The layer that translates between business objects and data stores.

This is a tried-and-true design for handling compounding complexity.

We’ve already seen the Repository class. Now let’s see the Service class (find the full class listing on my repository):


// src/main/java/com/example/iwreactspring/service/TodoService.java 
package com.example.iwreactspring.service;

import java.util.List;
import java.util.ArrayList;
import com.example.iwreactspring.model.TodoItem;
import org.springframework.stereotype.Service;

import org.springframework.beans.factory.annotation.Autowired;
import com.mongodb.client.MongoClient;
import com.mongodb.client.MongoClients;
import com.mongodb.client.MongoCollection;
import com.mongodb.client.MongoDatabase;
import org.bson.codecs.configuration.CodecRegistry;
import org.bson.codecs.pojo.PojoCodecProvider;
import org.bson.Document;

import com.example.iwreactspring.repository.TodoRepository;

@Service
public class TodoService {

  @Autowired
  private TodoRepository todoRepository;

  public List<TodoItem> getTodos() {
    return todoRepository.findAll();
  }

  public TodoItem createTodo(TodoItem newTodo) {
    TodoItem savedTodo = todoRepository.save(newTodo);
    return savedTodo;
  }
  public TodoItem getTodo(String id) {
    return todoRepository.findById(id).orElse(null);
  }

  public boolean deleteTodo(String id) {
    TodoItem todoToDelete = getTodo(id);
    if (todoToDelete != null) {
      todoRepository.deleteById(id);
      return true;
    } else {
      return false;
    }
  }
  public TodoItem saveTodo(TodoItem todoItem) {
    TodoItem savedTodo = todoRepository.save(todoItem);
    return savedTodo;
  }
}

We annotate this class with @Service to denote it as a service class. Again, this is not strictly required, because Spring can use the class as an injected bean without the annotation, but annotating the class makes things more descriptive. Next, we use @AutoWired to bring the TodoRepository class in. This will be populated by Spring based on the class type, which is the com.example.iwreactspring.repository.TodoRepository we saw earlier.

By default, Spring uses singleton injection (one instance of the injected bean class), which works well for us.

CRUD operations on the service class

Each method on this class is dedicated to performing one CRUD operation using the repository. For example, we need a way to get all the to-dos in the database, and getTodos() does that for us. The Repository class makes it very easy, too: return todoRepository.findAll() returns all the records (aka documents) in the todo collection (aka, database).

The other CRUD operations follow similar outlines:

  • Create a to-do item: todoRepository.save(newTodo)
  • Get a to-do item: todoRepository.findById(id).orElse(null)
  • Delete a to-do item: todoRepository.deleteById(id)

All these methods could be made more elaborate to handle error conditions or circumstances where records are not found. The delete method has a simple demonstration where it returns true if the record was successfully deleted, or false if not. We’ll use this in the controller to return an HTTP response, which the front end will use to display a message to the user.

The controller class

Now we just need to modify the Controller class to use our new service methods:


// src/main/java/com/example/iwreactspring/controller/MyController.java 
package com.example.iwreactspring.controller;

// other imports

import com.example.iwreactspring.model.TodoItem;
import com.example.iwreactspring.service.TodoService;

@RestController
public class MyController {

  @Autowired
  private TodoService todoService;

  @GetMapping("/todos")
  public ResponseEntity<List<TodoItem>> getTodos() {
    System.out.println("get todos: " + todoService.getTodos().size());
    return new ResponseEntity<>(todoService.getTodos(), HttpStatus.OK);
  }

  @PostMapping("/todos")
  public ResponseEntity<TodoItem> createTodo(@RequestBody TodoItem newTodo) {
    TodoItem todo = todoService.createTodo(newTodo);
    return new ResponseEntity<>(todo, HttpStatus.CREATED);
  }

  // Update (toggle completion) a TODO item
  @PutMapping("/todos/{id}")
  public ResponseEntity<TodoItem> updateTodoCompleted(@PathVariable String id) {
    System.out.println("BEGIN update: " + id);

    TodoItem todo = todoService.getTodo(id);
    if (todo != null) {
      todo.setCompleted(!todo.isCompleted());
      todoService.saveTodo(todo);
      return new ResponseEntity<>(HttpStatus.NO_CONTENT);
    } else {
      return new ResponseEntity<>(HttpStatus.NOT_FOUND);
    }
  }
  @DeleteMapping("/todos/{id}")
  public ResponseEntity<Void> deleteTodo(@PathVariable String id) {
    System.out.println("BEGIN delete: " + id);
    boolean deleted = todoService.deleteTodo(id);
    System.out.println("deleted: " +deleted);
    if (deleted) {
      return new ResponseEntity<>(HttpStatus.NO_CONTENT);
    } else {
      return new ResponseEntity<>(HttpStatus.NOT_FOUND);
    }
  }
}

(See the complete controller class with imports here.)

The Controller class is still annotated as a @RestController, so we can use HTTP mappings like @GetMapping and @DeleteMapping to handle requests and return custom HTTP responses. We’ve already covered the actual HTTP conversation in Part 1, but here we’re relying on the service methods to do the work.

For example, the deletion is handled by calling todoService.deleteTodo(id).

You might have noticed that our IDs in Part 1 were integers, but now they’re strings. These strings are generated by MongoDB when they are inserted into the collection. Because the front end didn’t depend on the IDs in any way, we didn’t have to make any changes. In fact, the front end stays the same and continues to function perfectly. That is one of the beauties of loose coupling between the client and server in a REST(ish) API. As long as the signature on the server stays the same, the client doesn’t care how the server does its work.

Conclusion

We now have a full-stack application built with React, Spring, and MongoDB, which lets us do all the CRUD operations necessary for a Todo app. These are all the architectural components required for almost any scope of application. You’ve seen here how all the application layers came together for data persistence. In the final article in this series, I’ll show you how to deploy the components to a production environment.

Want the complete code for the example app? Find it here.