This tutorial guides you through building a full-stack task management application using Spring Boot (backend) with Java 21, Open Liberty as the application server, and React.js (frontend). The app supports CRUD operations (Create, Read, Update, Delete) for tasks. We’ll use an H2 in-memory database for simplicity, but you can adapt it to other databases like PostgreSQL. Each code block includes detailed descriptions and inline comments to clarify functionality. Diagrams illustrate the architecture, and external resources are linked for beginners.
Prerequisites
- Java 21 (e.g., OpenJDK or Oracle JDK)
- Node.js and npm (version 18 or later, download from nodejs.org)
- Maven (for building Spring Boot, see Maven Getting Started)
- Open Liberty (application server, see Open Liberty Guides)
- An IDE like IntelliJ IDEA or VS Code
- Basic knowledge of Java, Spring Boot, and React.js (see Spring Boot Guides and React Docs)
Project Overview
- Backend: A Spring Boot application with RESTful API endpoints for task management, running on Open Liberty.
- Frontend: A React.js app that interacts with the backend via HTTP requests.
- Database: H2 in-memory database (learn more at H2 Database).
- Features:
- List all tasks
- Create a new task
- Update an existing task
- Delete a task
Architecture Diagram
Below is a diagram showing the application architecture:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
+-------------------+ HTTP Requests +-------------------+ | | <---------------------> | | | React.js | | Spring Boot | | Frontend | | Backend | | (localhost:3000) | | (localhost:8080) | | | | | | - TaskList | | - TaskController | | - Axios (HTTP) | | - TaskRepository | | | | - Task Entity | +-------------------+ +-------------------+ | v +-------------------+ | | | H2 Database | | (In-Memory) | | | +-------------------+ |
Explanation:
- The React.js frontend sends HTTP requests (GET, POST, PUT, DELETE) to the Spring Boot backend.
- The Spring Boot backend processes requests via the TaskController, uses TaskRepository to interact with the H2 database, and returns JSON responses.
- Open Liberty serves as the application server, hosting the Spring Boot app.
- The H2 database stores task data in memory during runtime.
For more on full-stack architecture, see Baeldung’s Full-Stack Guide.
Step 1: Set Up the Spring Boot Backend
1.1 Create a Spring Boot Project
- Visit Spring Initializr to generate a project with:
- Project: Maven
- Language: Java
- Spring Boot: 3.3.x (latest stable as of April 2025)
- Java: 21
- Dependencies:
- Spring Web (for REST APIs)
- Spring Data JPA (for database operations)
- H2 Database (in-memory database)
- Download and extract the zip file.
- Import the project into your IDE.
Why? Spring Initializr simplifies project setup by generating a pre-configured Spring Boot application. For beginners, see Spring Initializr Guide.
1.2 Configure Open Liberty
Open Liberty is a lightweight, open-source application server compatible with Spring Boot. Since Spring Initializr may not directly support Open Liberty, we configure it manually.
Add Open Liberty Dependency: Update pom.xml to include the Open Liberty runtime.xml
[xml]<!– Define dependencies for the project –> <dependencies> <!– Open Liberty runtime for running the Spring Boot app –> <dependency> <groupId>io.openliberty</groupId> <artifactId>openliberty-runtime</artifactId> <version>24.0.0.10</version> <type>pom</type> <scope>provided</scope> <!– Provided by the server, not bundled in the app –> </dependency> <!– Other dependencies (e.g., Spring Web, JPA) are already included by Initializr –> </dependencies>[/xml]
Description: This dependency ensures the app can run on Open Liberty. The provided scope means Open Liberty provides the runtime libraries. Learn more at Open Liberty Documentation.
Configure Liberty Maven Plugin: Add the plugin to pom.xml to manage the Open Liberty server.xml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
<!-- Define build plugins --> <build> <plugins> <!-- Liberty Maven plugin for running and deploying to Open Liberty --> <plugin> <groupId>io.openliberty.tools</groupId> <artifactId>liberty-maven-plugin</artifactId> <version>3.10</version> <configuration> <!-- Name of the Liberty server instance --> <serverName>defaultServer</serverName> </configuration> </plugin> </plugins> </build> |
Description: The liberty-maven-plugin allows you to start, stop, and deploy the app to Open Liberty via Maven commands. See Liberty Maven Plugin Guide.
Create server.xml: In src/main/liberty/config, create server.xml to configure the Open Liberty server.xml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
<!-- XML declaration and server configuration --> <?xml version="1.0" encoding="UTF-8"?> <server description="Spring Boot Server"> <!-- Enable features required for the app --> <featureManager> <!-- Support for Jakarta EE 10 APIs (e.g., JPA, JAX-RS) --> <feature>jakartaee-10.0</feature> <!-- Support for Spring Boot applications --> <feature>springBoot-3.0</feature> </featureManager> <!-- Configure HTTP and HTTPS ports --> <httpEndpoint id="defaultHttpEndpoint" host="*" httpPort="8080" httpsPort="8443"/> <!-- Automatically expand WAR files for deployment --> <applicationManager autoExpand="true"/> </server> |
Description: This file configures Open Liberty to support Jakarta EE 10 (for JPA and REST) and Spring Boot 3. It sets the server to listen on port 8080 for HTTP requests. For more, see Open Liberty Configuration.
Enable Java 21: Ensure pom.xml specifies Java 21.xml
1 2 3 4 5 |
<!-- Project properties --> <properties> <!-- Use Java 21 for compilation and runtime --> <java.version>21</java.version> </properties> |
Description: This ensures the project compiles and runs with Java 21 features (e.g., records, sealed classes). Learn about Java 21 at Oracle’s Java 21 Guide.
1.3 Create the Task Entity
Create a Task entity in com.example.demo.model to represent a task in the database.
java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 |
package com.example.demo.model; // Package declaration</em> import jakarta.persistence.Entity; // Import for marking class as JPA entity</em> import jakarta.persistence.GeneratedValue; // Import for auto-generating ID</em> import jakarta.persistence.GenerationType; // Import for ID generation strategy</em> import jakarta.persistence.Id; // Import for marking primary key</em> // Mark this class as a JPA entity (maps to a database table)</em> @Entity public class Task { // Mark this field as the primary key</em> @Id // Auto-generate the ID using an incrementing strategy</em> @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; // Unique identifier for the task</em> private String title; // Task title</em> private String description; // Task description</em> private boolean completed; // Task completion status</em> // Getter for ID</em> public Long getId() { return id; } // Setter for ID</em> public void setId(Long id) { this.id = id; } // Getter for title</em> public String getTitle() { return title; } // Setter for title</em> public void setTitle(String title) { this.title = title; } // Getter for description</em> public String getDescription() { return description; } // Setter for description</em> public void setDescription(String description) { this.description = description; } // Getter for completed status</em> public boolean isCompleted() { return completed; } // Setter for completed status</em> public void setCompleted(boolean completed) { this.completed = completed; } } |
Description: This class defines the Task entity, which maps to a database table via JPA annotations. @Entity indicates it’s a database entity, @Id marks the primary key, and @GeneratedValue auto-increments the ID. The fields (id, title, description, completed) represent task attributes. For more on JPA, see Spring Data JPA Guide.
1.4 Create the Repository
Create a TaskRepository interface in com.example.demo.repository to interact with the database.
java
1 2 3 4 5 6 7 8 9 |
package com.example.demo.repository; // Package declaration</em> import com.example.demo.model.Task; // Import Task entity</em> import org.springframework.data.jpa.repository.JpaRepository; // Import JPA repository interface</em> // Extend JpaRepository to get CRUD methods for Task entities</em> public interface TaskRepository extends JpaRepository<Task, Long> { // No additional methods needed; JpaRepository provides findAll, save, etc.</em> } |
Description: This interface extends JpaRepository, which provides built-in CRUD methods (e.g., findAll(), save(), deleteById()). The type parameters <Task, Long> indicate it manages Task entities with a Long ID. Learn more at Spring Data JPA Docs.
1.5 Create the REST Controller
Create a TaskController in com.example.demo.controller to handle HTTP requests.
java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 |
package com.example.demo.controller; // Package declaration</em> import com.example.demo.model.Task; // Import Task entity</em> import com.example.demo.repository.TaskRepository; // Import Task repository</em> import org.springframework.beans.factory.annotation.Autowired; // Import for dependency injection</em> import org.springframework.http.ResponseEntity; // Import for HTTP responses</em> import org.springframework.web.bind.annotation.*; // Import for REST annotations</em> import java.util.List; // Import for List type</em> import java.util.Optional; // Import for Optional type</em> // Mark this class as a REST controller</em> @RestController // Map all endpoints to /api/tasks</em> @RequestMapping("/api/tasks") // Allow CORS requests from React frontend</em> @CrossOrigin(origins = "http://localhost:3000") public class TaskController { // Inject TaskRepository via dependency injection</em> @Autowired private TaskRepository taskRepository; // Handle GET /api/tasks to fetch all tasks</em> @GetMapping public List<Task> getAllTasks() { // Return all tasks from the database</em> return taskRepository.findAll(); } // Handle GET /api/tasks/{id} to fetch a task by ID</em> @GetMapping("/{id}") public ResponseEntity<Task> getTaskById(@PathVariable Long id) { // Find task by ID</em> Optional<Task> task = taskRepository.findById(id); // Return task if found, else return 404 Not Found</em> return task.map(ResponseEntity::ok).orElseGet(() -> ResponseEntity.notFound().build()); } // Handle POST /api/tasks to create a new task</em> @PostMapping public Task createTask(@RequestBody Task task) { // Save the task to the database and return it</em> return taskRepository.save(task); } // Handle PUT /api/tasks/{id} to update a task</em> @PutMapping("/{id}") public ResponseEntity<Task> updateTask(@PathVariable Long id, @RequestBody Task updatedTask) { // Find task by ID</em> Optional<Task> task = taskRepository.findById(id); if (task.isPresent()) { // Update existing task's fields</em> Task existingTask = task.get(); existingTask.setTitle(updatedTask.getTitle()); existingTask.setDescription(updatedTask.getDescription()); existingTask.setCompleted(updatedTask.isCompleted()); // Save and return updated task</em> return ResponseEntity.ok(taskRepository.save(existingTask)); } // Return 404 if task not found</em> return ResponseEntity.notFound().build(); } // Handle DELETE /api/tasks/{id} to delete a task</em> @DeleteMapping("/{id}") public ResponseEntity<Void> deleteTask(@PathVariable Long id) { // Check if task exists</em> if (taskRepository.existsById(id)) { // Delete task by ID</em> taskRepository.deleteById(id); // Return 200 OK</em> return ResponseEntity.ok().build(); } // Return 404 if task not found</em> return ResponseEntity.notFound().build(); } } |
Description: This controller defines REST endpoints for task management:
- GET /api/tasks: Fetch all tasks.
- GET /api/tasks/{id}: Fetch a task by ID.
- POST /api/tasks: Create a new task.
- PUT /api/tasks/{id}: Update a task.
- DELETE /api/tasks/{id}: Delete a task. The @CrossOrigin annotation allows requests from the React frontend. ResponseEntity handles HTTP status codes (e.g., 200 OK, 404 Not Found). For more on REST APIs, see Spring REST Guide.
1.6 Configure Application Properties
Update src/main/resources/application.properties to configure the H2 database and server port.
properties
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
# Configure H2 in-memory database URL</em> spring.datasource.url=jdbc:h2:mem:testdb # Specify H2 driver class</em> spring.datasource.driverClassName=org.h2.Driver # Set database username</em> spring.datasource.username=sa # Set database password (empty for simplicity)</em> spring.datasource.password= # Use H2 dialect for JPA</em> spring.jpa.database-platform=org.hibernate.dialect.H2Dialect # Enable H2 console for debugging (accessible at /h2-console)</em> spring.h2.console.enabled=true # Set server port to 8080</em> server.port=8080 |
Description: This file configures the H2 database connection and enables the H2 console for viewing data. The server runs on port 8080 to avoid conflicts with the React frontend (port 3000). Learn more at Spring Boot Properties Guide.
1.7 Run the Backend
Start the Open Liberty server:
bash
1 |
mvn liberty:run |
The backend will be available at http://localhost:8080. Test the API using Postman or curl:
bash
1 |
curl http://localhost:8080/api/tasks |
You can also access the H2 console at http://localhost:8080/h2-console (use the JDBC URL jdbc:h2:mem:testdb).
Why? The liberty:run command starts Open Liberty and deploys the Spring Boot app. For more, see Open Liberty Maven Guide.
Step 2: Set Up the React.js Frontend
2.1 Create a React App
- In a terminal, create a new React app:bash
npx create-react-app task-manager-frontend cd task-manager-frontend
- Install dependencies for HTTP requests and styling:bash
npm install axios bootstrap
Description: create-react-app sets up a React project with a development server and build tools. axios handles HTTP requests, and bootstrap provides CSS styling. For beginners, see Create React App Docs and Axios Guide.
2.2 Create the Task Component
Create src/components/TaskList.js to display and manage tasks.
jsx
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 |
// Import React and hooks for state and lifecycle management</em> import React, { useState, useEffect } from 'react'; // Import axios for HTTP requests</em> import axios from 'axios'; // Define the TaskList component</em> const TaskList = () => { // State to store the list of tasks</em> const [tasks, setTasks] = useState([]); // State to store form input for creating a new task</em> const [newTask, setNewTask] = useState({ title: '', description: '', completed: false }); // Fetch tasks when the component mounts</em> useEffect(() => { // Send GET request to fetch all tasks</em> axios.get('http://localhost:8080/api/tasks') .then(response => setTasks(response.data)) // Update tasks state with response data</em> .catch(error => console.error('Error fetching tasks:', error)); // Log errors</em> }, []); // Empty dependency array means this runs once on mount</em> // Handle creating a new task</em> const handleCreate = () => { // Send POST request to create a task</em> axios.post('http://localhost:8080/api/tasks', newTask) .then(response => { // Add new task to the tasks state</em> setTasks([...tasks, response.data]); // Reset form inputs</em> setNewTask({ title: '', description: '', completed: false }); }) .catch(error => console.error('Error creating task:', error)); // Log errors</em> }; // Handle updating a task</em> const handleUpdate = (id, updatedTask) => { // Send PUT request to update the task</em> axios.put(`http://localhost:8080/api/tasks/${id}`, updatedTask) .then(response => { // Update tasks state with the modified task</em> setTasks(tasks.map(task => (task.id === id ? response.data : task))); }) .catch(error => console.error('Error updating task:', error)); // Log errors</em> }; // Handle deleting a task</em> const handleDelete = (id) => { // Send DELETE request to remove the task</em> axios.delete(`http://localhost:8080/api/tasks/${id}`) .then(() => { // Remove the task from the tasks state</em> setTasks(tasks.filter(task => task.id !== id)); }) .catch(error => console.error('Error deleting task:', error)); // Log errors</em> }; // Render the component</em> return ( <div className="container"> <h1>Task Manager</h1> {/* Form for creating a new task */</em>} <div className="mb-3"> {/* Input for task title */</em>} <input type="text" className="form-control" placeholder="Title" value={newTask.title} onChange={e => setNewTask({ ...newTask, title: e.target.value })} // Update title in state</em> /> {/* Input for task description */</em>} <input type="text" className="form-control mt-2" placeholder="Description" value={newTask.description} onChange={e => setNewTask({ ...newTask, description: e.target.value })} // Update description in state</em> /> {/* Button to submit new task */</em>} <button className="btn btn-primary mt-2" onClick={handleCreate}>Add Task</button> </div> {/* List of tasks */</em>} <ul className="list-group"> {tasks.map(task => ( <li key={task.id} className="list-group-item d-flex justify-content-between"> <div> <h5>{task.title}</h5> <p>{task.description}</p> {/* Checkbox to toggle task completion */</em>} <input type="checkbox" checked={task.completed} onChange={() => handleUpdate(task.id, { ...task, completed: !task.completed })} // Toggle completed status</em> /> </div> {/* Button to delete task */</em>} <button className="btn btn-danger" onClick={() => handleDelete(task.id)}>Delete</button> </li> ))} </ul> </div> ); }; // Export the component</em> export default TaskList; |
Description: This component:
- Uses useState to manage tasks and form input state.
- Uses useEffect to fetch tasks when the component mounts.
- Defines functions (handleCreate, handleUpdate, handleDelete) to send HTTP requests via Axios.
- Renders a form to create tasks and a list of tasks with checkboxes and delete buttons. Bootstrap classes (container, form-control, btn) provide styling. For more on React hooks, see React Hooks Guide.
2.3 Update App.js
Update src/App.js to include the TaskList component and Bootstrap CSS.
jsx
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
// Import React</em> import React from 'react'; // Import Bootstrap CSS for styling</em> import 'bootstrap/dist/css/bootstrap.min.css'; // Import TaskList component</em> import TaskList from './components/TaskList'; // Define the App component</em> function App() { return ( <div className="App"> {/* Render the TaskList component */</em>} <TaskList /> </div> ); } // Export the App component</em> export default App; |
Description: This is the root component that imports Bootstrap CSS and renders the TaskList component. For more on React components, see React Components Guide.
2.4 Run the Frontend
Start the React app:
bash
1 |
npm start |
The frontend will be available at http://localhost:3000.
Why? The npm start command runs the React development server, which supports hot reloading for faster development. See Create React App Docs.
Step 3: Test the Application
- Ensure the backend (Open Liberty) is running on http://localhost:8080.
- Ensure the frontend (React) is running on http://localhost:3000.
- Open http://localhost:3000 in your browser.
- Test the following:
- Add a new task by entering a title and description, then clicking “Add Task.”
- Mark a task as completed by checking its checkbox.
- Delete a task by clicking its “Delete” button.
The frontend communicates with the backend via REST APIs, and changes are stored in the H2 database (in-memory).
Data Flow Diagram:
1 2 3 4 5 6 7 8 9 |
React Frontend Spring Boot Backend H2 Database | | | |--- GET /api/tasks ------> | | | |--- findAll() -------------> | | <--- JSON Response -------| | | | | |--- POST /api/tasks -----> | | | |--- save() ----------------> | | <--- JSON Response -------| | |
Explanation: The frontend sends HTTP requests to the backend, which uses JPA to interact with the H2 database. Responses are sent back as JSON.
Step 4: Deploying the Application (Optional)
To deploy the app:
- Backend:
- Package the Spring Boot app with Open Liberty:bash
mvn clean package liberty:package
- The generated .war file (in target/) can be deployed to an Open Liberty server in production. See Open Liberty Deployment Guide.
- Package the Spring Boot app with Open Liberty:bash
- Frontend:
- Build the React app:bash
npm run build
- Serve the build folder using a static file server like Nginx or bundle it with the backend. See React Deployment Guide.
- Build the React app:bash
Conclusion
You’ve built a full-stack task management application using Spring Boot with Java 21, Open Liberty, and React.js. The backend provides a RESTful API, and the frontend offers a user-friendly interface. Each code block was explained with detailed comments to clarify its purpose.
Next Steps
- Add user authentication with Spring Security.
- Switch to a persistent database like PostgreSQL.
- Enhance the UI with Material-UI or Tailwind CSS.
For further learning, explore:
- Spring Boot Documentation
- Open Liberty Guides
- React Documentation
- Baeldung’s Spring Tutorials
- FreeCodeCamp’s React Guide
Happy coding! Let me know if you need help with extensions or debugging.