Explore the popular and powerful React + Java + Spring stack by incorporating a service layer and a MongoDB instance for data persistence. 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. Related content news Wasmer WebAssembly platform now backs iOS Wasmer 5.0 release also features improved performance, a leaner codebase, and discontinued support for the Emscripten toolchain. By Paul Krill Oct 30, 2024 2 mins Mobile Development Web Development Software Development news analysis What Entrust certificate distrust means for developers Secure communications between web browsers and web servers depend on digital certificates backed by certificate authorities. What if the web browsers stop trusting your CA? By Travis Van Oct 30, 2024 9 mins Browser Security Web Development Application Security news Next.js 15 arrives with faster bundler High-performance Rust-based Turbopack bundler moves from beta to stable with the latest update of the React-based web framework. By Paul Krill Oct 24, 2024 2 mins JavaScript React Web Development feature WasmGC and the future of front-end Java development WebAssembly’s garbage collection extension makes it easier to run languages like Java on the front end. Could it be the start of a new era in web development? By Matthew Tyson Oct 16, 2024 10 mins Web Development Software Development Resources Videos