Spring Boot React Project - Todo App

In this step-by-step tutorial, we will build a simple CRUD Todo application using Java, Spring Boot, JavaScript, React JS, and MySQL database.

Let's first build REST APIs for the Todo app using Spring Boot, and then we will build the React App to consume the REST APIs.

Spring Boot React Project

Create Spring Boot Project and Import in IDE

Let's use the Spring Initializr tool to quickly create and set up the Spring boot application.

Add Maven Dependencies

Add below Maven dependencies to the pom.xml file:

		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-data-jpa</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
		</dependency>

		<dependency>
			<groupId>com.mysql</groupId>
			<artifactId>mysql-connector-j</artifactId>
			<scope>runtime</scope>
		</dependency>
		<dependency>
			<groupId>org.projectlombok</groupId>
			<artifactId>lombok</artifactId>
			<optional>true</optional>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-test</artifactId>
			<scope>test</scope>
		</dependency>

		<!-- https://mvnrepository.com/artifact/org.modelmapper/modelmapper -->
		<dependency>
			<groupId>org.modelmapper</groupId>
			<artifactId>modelmapper</artifactId>
			<version>3.1.1</version>
		</dependency>

Configure MySQL Database

spring.datasource.url=jdbc:mysql://localhost:3306/todo_management
spring.datasource.username=root
spring.datasource.password=Mysql@123

spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQLDialect
spring.jpa.hibernate.ddl-auto=update

Create Todo Entity

The Java class Todo represents a database entity for managing todo items, using JPA annotations to map it to a table named todos in a database. The class utilizes Lombok annotations to automatically generate getters, setters, a no-argument constructor, and an all-argument constructor. Each todo item has an id (automatically generated as a unique identifier), a title, a description (both required fields due to nullable = false), and a completed status to indicate if the task is finished.

Let's create a Todo JPA entity and add the following code to it:
package net.javaguides.todo.entity;

import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

@Setter
@Getter
@NoArgsConstructor
@AllArgsConstructor
@Entity
@Table(name = "todos")
public class Todo {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false)
    private String title;

    @Column(nullable = false)
    private String description;
    private boolean completed;
}

Create TodoRepository

Let's create TodoRepository interface and add the following content to it:
package net.javaguides.todo.repository;

import net.javaguides.todo.entity.Todo;
import org.springframework.data.jpa.repository.JpaRepository;

public interface TodoRepository extends JpaRepository<Todo, Long> {
}
The TodoRepository interface is a component in Spring Data JPA that defines a data access layer for the Todo entity. Extending JpaRepository inherits a comprehensive suite of methods to handle CRUD operations (create, read, update, and delete) for Todo objects, where Todo serves as the entity type. Long is the type of its primary key. This abstraction allows for easy and efficient interaction with the database without the need to write detailed database queries or manage transactional code, simplifying the development process.

Create TodoDto

Let's create a TodoDto class to transfer the data between client and server.
package net.javaguides.todo.dto;

import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

@Setter
@Getter
@NoArgsConstructor
@AllArgsConstructor
public class TodoDto {

    private Long id;
    private String title;
    private String description;
    private boolean completed;
}
The TodoDto class is a simple Data Transfer Object (DTO) in Java that uses Lombok annotations to automatically generate setters, getters, a no-argument constructor, and an all-argument constructor. This class is designed to encapsulate and transport data about todo items, including an id, a title, a description, and a completed status, between different layers of an application, such as the service layer and the client.

Create Custom Exception - ResourceNotFoundException

Let's create a ResourceNotFoundException class and add the following code to it:
package net.javaguides.todo.exception;

import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ResponseStatus;

@ResponseStatus(value = HttpStatus.NOT_FOUND)
public class ResourceNotFoundException extends RuntimeException{

    public ResourceNotFoundException(String message) {
        super(message);
    }
}
The ResourceNotFoundException is a custom exception class in a Spring application that extends RuntimeException. It is marked with the @ResponseStatus(HttpStatus.NOT_FOUND) annotation, instructing Spring to return an HTTP 404 Not Found status whenever this exception is thrown. This setup is typically used to handle scenarios where a requested resource is unavailable, providing a clear and standardized response to the client.

Create Service Layer - TodoService Interface

Let's create a TodoService interface and add the following code to it:
package net.javaguides.todo.service;

import net.javaguides.todo.dto.TodoDto;

import java.util.List;

public interface TodoService {

    TodoDto addTodo(TodoDto todoDto);

    TodoDto getTodo(Long id);

    List<TodoDto> getAllTodos();

    TodoDto updateTodo(TodoDto todoDto, Long id);

    void deleteTodo(Long id);

    TodoDto completeTodo(Long id);

    TodoDto inCompleteTodo(Long id);
}
The TodoService interface defines the contract for a service layer in a Spring application, managing operations related to TodoDto objects. 

This interface specifies methods for various CRUD operations:
  • addTodo: Adds a new todo item and returns the added TodoDto.
  • getTodo: Retrieves a specific todo item by its ID.
  • getAllTodos: Returns a list of all todo items.
  • updateTodo: Updates an existing todo item based on the provided ID and returns the updated TodoDto.
  • deleteTodo: Deletes a todo item by its ID.
  • completeTodo: Marks a todo item as completed and returns the updated TodoDto.
  • inCompleteTodo: Marks a todo item as incomplete and returns the updated TodoDto.
These methods facilitate interaction between the controller and the data access layers, abstracting the business logic for managing todo items.

Create Service Layer - TodoServiceImpl class

Let's create a TodoServiceImpl class and add the following code to it:
package net.javaguides.todo.service.impl;

import lombok.AllArgsConstructor;
import net.javaguides.todo.dto.TodoDto;
import net.javaguides.todo.entity.Todo;
import net.javaguides.todo.exception.ResourceNotFoundException;
import net.javaguides.todo.repository.TodoRepository;
import net.javaguides.todo.service.TodoService;
import org.modelmapper.ModelMapper;
import org.springframework.stereotype.Service;

import java.util.List;
import java.util.stream.Collectors;

@Service
@AllArgsConstructor
public class TodoServiceImpl implements TodoService {

    private TodoRepository todoRepository;

    private ModelMapper modelMapper;

    @Override
    public TodoDto addTodo(TodoDto todoDto) {

        // convert TodoDto into Todo Jpa entity
        Todo todo = modelMapper.map(todoDto, Todo.class);

        // Todo Jpa entity
        Todo savedTodo = todoRepository.save(todo);

        // Convert saved Todo Jpa entity object into TodoDto object

        TodoDto savedTodoDto = modelMapper.map(savedTodo, TodoDto.class);

        return savedTodoDto;
    }

    @Override
    public TodoDto getTodo(Long id) {

        Todo todo = todoRepository.findById(id)
                .orElseThrow(() -> new ResourceNotFoundException("Todo not found with id:" + id));

        return modelMapper.map(todo, TodoDto.class);
    }

    @Override
    public List<TodoDto> getAllTodos() {

        List<Todo> todos = todoRepository.findAll();

        return todos.stream().map((todo) -> modelMapper.map(todo, TodoDto.class))
                .collect(Collectors.toList());
    }

    @Override
    public TodoDto updateTodo(TodoDto todoDto, Long id) {

         Todo todo = todoRepository.findById(id)
                .orElseThrow(() -> new ResourceNotFoundException("Todo not found with id : " + id));
         todo.setTitle(todoDto.getTitle());
         todo.setDescription(todoDto.getDescription());
         todo.setCompleted(todoDto.isCompleted());

         Todo updatedTodo = todoRepository.save(todo);

        return modelMapper.map(updatedTodo, TodoDto.class);
    }

    @Override
    public void deleteTodo(Long id) {

        Todo todo = todoRepository.findById(id)
                .orElseThrow(() -> new ResourceNotFoundException("Todo not found with id : " + id));

        todoRepository.deleteById(id);
    }

    @Override
    public TodoDto completeTodo(Long id) {

        Todo todo = todoRepository.findById(id)
                .orElseThrow(() -> new ResourceNotFoundException("Todo not found with id : " + id));

        todo.setCompleted(Boolean.TRUE);

        Todo updatedTodo = todoRepository.save(todo);

        return modelMapper.map(updatedTodo, TodoDto.class);
    }

    @Override
    public TodoDto inCompleteTodo(Long id) {

        Todo todo = todoRepository.findById(id)
                .orElseThrow(() -> new ResourceNotFoundException("Todo not found with id : " + id));

        todo.setCompleted(Boolean.FALSE);

        Todo updatedTodo = todoRepository.save(todo);

        return modelMapper.map(updatedTodo, TodoDto.class);
    }
}

The TodoServiceImpl class implements the TodoService interface and is marked with the @Service annotation, indicating that it's a Spring service layer component. This class manages the business logic associated with todo operations, utilizing a TodoRepository for data access and a ModelMapper to convert between Todo entity objects and TodoDto data transfer objects. 

Here's a breakdown of its functionalities:

addTodo: Converts a TodoDto to a Todo entity, saves it using the repository, and then converts the saved entity back to a TodoDto to return.

getTodo: Fetches a Todo by its ID. If not found, it throws a ResourceNotFoundException. The found entity is then converted to a TodoDto.

getAllTodos: Retrieves all todos from the repository, converts each to TodoDto, and returns them as a list.

updateTodo: Finds an existing todo by ID (or throws if not found), updates its properties from the provided TodoDto, saves the updated entity, and returns it as a TodoDto.

deleteTodo: Looks up a todo by ID and deletes it, throwing ResourceNotFoundException if not found.

completeTodo and inCompleteTodo: Both methods look up a todo by ID, set its completed status to true or false respectively, save the updated todo, and return it as a TodoDto.

This service class encapsulates all data handling and conversion logic, ensuring that the controller layer interacts with clean and straightforward data transfer objects, abstracting away the database entity details.

Create REST Controller Layer - TodoController

The TodoController class defines REST API endpoints for managing todos in a Spring Boot application. It handles CRUD operations and status updates for todo items, leveraging a service layer (TodoService) to perform actions like adding, retrieving, updating, and deleting todos. The controller uses annotations to map HTTP requests to specific methods and returns the appropriate responses and HTTP status codes. Each method in the controller corresponds to a different HTTP operation, ensuring that the API is RESTful and can handle cross-origin requests from any domain.

Let's create a TodoController class and add the following code to it:
package net.javaguides.todo.controller;

import lombok.AllArgsConstructor;
import net.javaguides.todo.dto.TodoDto;
import net.javaguides.todo.service.TodoService;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.util.List;

@CrossOrigin("*")
@RestController
@RequestMapping("api/todos")
@AllArgsConstructor
public class TodoController {

    private TodoService todoService;

    // Build Add Todo REST API

    @PostMapping
    public ResponseEntity<TodoDto> addTodo(@RequestBody TodoDto todoDto){

        TodoDto savedTodo = todoService.addTodo(todoDto);

        return new ResponseEntity<>(savedTodo, HttpStatus.CREATED);
    }

    // Build Get Todo REST API
    @GetMapping("{id}")
    public ResponseEntity<TodoDto> getTodo(@PathVariable("id") Long todoId){
        TodoDto todoDto = todoService.getTodo(todoId);
        return new ResponseEntity<>(todoDto, HttpStatus.OK);
    }

    // Build Get All Todos REST API
    @GetMapping
    public ResponseEntity<List<TodoDto>> getAllTodos(){
        List<TodoDto> todos = todoService.getAllTodos();
        //return new ResponseEntity<>(todos, HttpStatus.OK);
        return ResponseEntity.ok(todos);
    }

    // Build Update Todo REST API
    @PutMapping("{id}")
    public ResponseEntity<TodoDto> updateTodo(@RequestBody TodoDto todoDto, @PathVariable("id") Long todoId){
        TodoDto updatedTodo = todoService.updateTodo(todoDto, todoId);
        return ResponseEntity.ok(updatedTodo);
    }

    // Build Delete Todo REST API
    @DeleteMapping("{id}")
    public ResponseEntity<String> deleteTodo(@PathVariable("id") Long todoId){
        todoService.deleteTodo(todoId);
        return ResponseEntity.ok("Todo deleted successfully!.");
    }

    // Build Complete Todo REST API
    @PatchMapping("{id}/complete")
    public ResponseEntity<TodoDto> completeTodo(@PathVariable("id") Long todoId){
        TodoDto updatedTodo = todoService.completeTodo(todoId);
        return ResponseEntity.ok(updatedTodo);
    }

    // Build In Complete Todo REST API
    @PatchMapping("{id}/in-complete")
    public ResponseEntity<TodoDto> inCompleteTodo(@PathVariable("id") Long todoId){
        TodoDto updatedTodo = todoService.inCompleteTodo(todoId);
        return ResponseEntity.ok(updatedTodo);
    }

}

Create React App

Run the following command to create a new React app using Vite:
npm create vite@latest todo-ui

Let's break down the command:

npm: This is the command-line interface for Node Package Manager (npm).

create-vite: It is a package provided by Vite that allows you to scaffold a new Vite project.

@latest: This specifies that the latest version of Vite should be installed.

todo-ui: This is the name you choose for your app. You can replace it with your desired app name.

Adding Bootstrap in React Using NPM

Open a new terminal window, navigate to your project's folder, and run the following command:

$ npm install bootstrap --save

--save option add an entry in the package.json file

Open the src/main.js file and add the following code:

import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App.jsx'
import './index.css'
import 'bootstrap/dist/css/bootstrap.min.css'

ReactDOM.createRoot(document.getElementById('root')).render(
  <React.StrictMode>
    <App />
  </React.StrictMode>,
)

Connect React App to Todo REST APIs

We will use the Axios HTTP library to make REST API calls in our React application. Let's install Axios using the NPM command below:

npm add axios --save

Let's create a TodoService.js file and add the following code to it:

import axios from "axios";

const BASE_REST_API_URL = 'http://localhost:8080/api/todos';

// export function getAllTodos(){
//     return axios.get(BASE_REST_API_URL);
// }

export const getAllTodos = () => axios.get(BASE_REST_API_URL)

export const saveTodo = (todo) => axios.post(BASE_REST_API_URL, todo)

export const getTodo = (id) => axios.get(BASE_REST_API_URL + '/' + id)

export const updateTodo = (id, todo) => axios.put(BASE_REST_API_URL + '/' + id, todo)

export const deleteTodo = (id) => axios.delete(BASE_REST_API_URL + '/' + id)

export const completeTodo = (id) => axios.patch(BASE_REST_API_URL + '/' + id + '/complete')

export const inCompleteTodo = (id) => axios.patch(BASE_REST_API_URL + '/' + id + '/in-complete')
This JavaScript module contains a set of API utility functions using axios to make HTTP requests to a backend server and manage todo items. These functions facilitate operations corresponding to RESTful services:

getAllTodos: Retrieves all todo items from the server by sending a GET request to the base API URL.

saveTodo: This function submits a new to-do item to the server by sending a POST request along with the to-do data to the base API URL.

getTodo: Fetches a specific todo item by its ID, appending the ID to the base URL and making a GET request.

updateTodo: This function updates a specific to-do item, identified by its ID, by sending a PUT request with the to-do data to the corresponding URL.

deleteTodo: This function deletes a specific to-do item by its ID by sending a DELETE request to the URL that includes the ID.

completeTodo: Marks a specific todo item as completed by sending a PATCH request to a URL constructed by appending /complete to the base URL with the ID.

inCompleteTodo: Marks a specific todo item as incomplete by sending a PATCH request to a URL that appends /in-complete to the base URL with the ID.

These utility functions streamline interacting with the API, handling data creation, retrieval, modification, and deletion operations for todos. They can be imported and used throughout a React application or any other client-side framework communicating with the server.

Configure Routing in React App

To use React Router, you first have to install it using NPM:

 npm install react-router-dom --save 

Let's open the App.jsx file and add the following content to it:

import { useState } from 'react'
import './App.css'
import ListTodoComponent from './components/ListTodoComponent'
import HeaderComponent from './components/HeaderComponent'
import FooterComponent from './components/FooterComponent'
import { BrowserRouter, Routes, Route} from 'react-router-dom'
import TodoComponent from './components/TodoComponent'

function App() {

  return (
    <>
    <BrowserRouter>
        <HeaderComponent />
          <Routes>
              {/* http://localhost:8080 */}
              <Route path='/' element = { <ListTodoComponent /> }></Route>
               {/* http://localhost:8080/todos */}
              <Route path='/todos' element = { <ListTodoComponent /> }></Route>
              {/* http://localhost:8080/add-todo */}
              <Route path='/add-todo' element = { <TodoComponent /> }></Route>
              {/* http://localhost:8080/update-todo/1 */}
              <Route path='/update-todo/:id' element = { <TodoComponent /> }></Route>

          </Routes>
        <FooterComponent />
        </BrowserRouter>
    </>
  )
}

export default App
The BrowserRouter component from react-router-dom is used to enable routing in the application. It wraps the entire application to provide routing context to the nested components.

HeaderComponent and FooterComponent are static components that likely render the application's header and footer, respectively. They are placed outside the Routes component, meaning they will remain visible on all pages.

The root path ('/') and the path ('/todos') both render the ListTodoComponent, which likely displays a list of all todo items. 

The path ('/add-todo') renders the TodoComponent in a mode probably set up for adding a new todo. 

The dynamic path ('/update-todo/:id') also renders the TodoComponent, but this time likely in a mode for updating an existing todo, where :id is a parameter representing the ID of the todo to be updated.

Create React ListTodoComponent

import React, { useEffect, useState } from 'react'
import { completeTodo, deleteTodo, getAllTodos, inCompleteTodo } from '../services/TodoService'
import { useNavigate } from 'react-router-dom'

const ListTodoComponent = () => {

    const [todos, setTodos] = useState([])

    const navigate = useNavigate()


    useEffect(() => {
        listTodos();
    }, [])

    function listTodos(){
        getAllTodos().then((response) => {
            setTodos(response.data);
        }).catch(error => {
            console.error(error);
        })
    }

    function addNewTodo(){
        navigate('/add-todo')

    }

    function updateTodo(id){
        console.log(id)
        navigate(`/update-todo/${id}`)
    }

    function removeTodo(id){
        deleteTodo(id).then((response) => {
            listTodos();
        }).catch(error => {
            console.error(error)
        })
    }

    function markCompleteTodo(id){
        completeTodo(id).then((response) => {
            listTodos()
        }).catch(error => {
            console.error(error)
        })
    }

    function markInCompleteTodo(id){
        inCompleteTodo(id).then((response) => {
            listTodos();
        }).catch(error => {
            console.error(error)
        })
    }

  return (
    <div className='container'>
        <h2 className='text-center'>List of Todos</h2>
        <button className='btn btn-primary mb-2' onClick={addNewTodo}>Add Todo</button>
        <div>
            <table className='table table-bordered table-striped'>
                <thead>
                    <tr>
                        <th>Todo Title</th>
                        <th>Todo Description</th>
                        <th>Todo Completed</th>
                        <th>Actions</th>
                    </tr>
                </thead>
                <tbody>
                    {
                        todos.map(todo =>
                            <tr key={todo.id}>
                                <td>{todo.title}</td>
                                <td>{todo.description}</td>
                                <td>{todo.completed ? 'YES': 'NO'}</td>
                                <td>
                                    <button className='btn btn-info' onClick={() => updateTodo(todo.id)}>Update</button>
                                    <button className='btn btn-danger' onClick={() => removeTodo(todo.id)} style={ { marginLeft: "10px" }} >Delete</button>
                                    <button className='btn btn-success' onClick={() => markCompleteTodo(todo.id)} style={ { marginLeft: "10px" }} >Complete</button>
                                    <button className='btn btn-info' onClick={() => markInCompleteTodo(todo.id)} style={ { marginLeft: "10px" }} >In Complete</button>
                                </td>
                            </tr>
                        )
                    }

                </tbody>
            </table>
        </div>

    </div>
  )
}

export default ListTodoComponent
The ListTodoComponent in React manages the display and operations of todo items. It uses hooks to handle state and navigation, fetching the list of todos on component mount via getAllTodos

The component provides functions to navigate to add and update routes, and to perform CRUD operations. These operations include deleting a todo, marking it as complete, or incomplete, with state updates after each operation to reflect changes. 

The UI includes buttons for adding new todos, updating existing ones, and changing their completion status, all managed within a user-friendly table layout.

Create React HeaderComponent

import React from 'react'

const HeaderComponent = () => {
  return (
    <div>
        <header>
            <nav className='navbar navbar-expand-md navbar-dark bg-dark'>
                <div>
                    <a href='http://localhost:3000' className='navbar-brand'>
                        Todo Management Application
                    </a>
                </div>
            </nav>
        </header>

    </div>
  )
}

export default HeaderComponent

Create React FooterComponent

import React from 'react'

const FooterComponent = () => {
  return (
    <div>
        <footer className='footer'>
            <p className='text-center'>Copyrights reserved at 2023-25 by Java Guides</p>
        </footer>
    </div>
  )
}

export default FooterComponent

Create React TodoComponent

import React, { useEffect } from 'react'
import { useState } from 'react'
import { getTodo, saveTodo, updateTodo } from '../services/TodoService'
import { useNavigate, useParams } from 'react-router-dom'

const TodoComponent = () => {

    const [title, setTitle] = useState('')
    const [description, setDescription] = useState('')
    const [completed, setCompleted] = useState(false)
    const navigate = useNavigate()
    const { id } = useParams()


    function saveOrUpdateTodo(e){
        e.preventDefault()

        const todo = {title, description, completed}
        console.log(todo);

        if(id){

            updateTodo(id, todo).then((response) => {
                navigate('/todos')
            }).catch(error => {
                console.error(error);
            })

        }else{
            saveTodo(todo).then((response) => {
                console.log(response.data)
                navigate('/todos')
            }).catch(error => {
                console.error(error);
            })
        }
    }

    function pageTitle(){
        if(id) {
            return <h2 className='text-center'>Update Todo</h2>
        }else {
            return <h2 className='text-center'>Add Todo</h2>
        }
    }

    useEffect( () => {

        if(id){
            getTodo(id).then((response) => {
                console.log(response.data)
                setTitle(response.data.title)
                setDescription(response.data.description)
                setCompleted(response.data.completed)
            }).catch(error => {
                console.error(error);
            })
        }

    }, [id])

  return (
    <div className='container'>
        <br /> <br />
        <div className='row'>
            <div className='card col-md-6 offset-md-3 offset-md-3'>
                { pageTitle() }
                <div className='card-body'>
                    <form>
                        <div className='form-group mb-2'>
                            <label className='form-label'>Todo Title:</label>
                            <input
                                type='text'
                                className='form-control'
                                placeholder='Enter Todo Title'
                                name='title'
                                value={title}
                                onChange={(e) => setTitle(e.target.value)}
                            >
                            </input>
                        </div>

                        <div className='form-group mb-2'>
                            <label className='form-label'>Todo Description:</label>
                            <input
                                type='text'
                                className='form-control'
                                placeholder='Enter Todo Description'
                                name='description'
                                value={description}
                                onChange={(e) => setDescription(e.target.value)}
                            >
                            </input>
                        </div>

                        <div className='form-group mb-2'>
                            <label className='form-label'>Todo Completed:</label>
                            <select
                                className='form-control'
                                value={completed}
                                onChange={(e) => setCompleted(e.target.value)}
                            >
                                <option value="false">No</option>
                                <option value="true">Yes</option>

                            </select>
                        </div>

                        <button className='btn btn-success' onClick={ (e) => saveOrUpdateTodo(e)}>Submit</button>
                    </form>

                </div>
            </div>

        </div>
    </div>
  )
}

export default TodoComponent
The TodoComponent in React is used for both adding new todos and updating existing ones, depending on whether an ID is present in the URL parameters. It initializes state variables for title, description, and completed, and uses hooks for navigation and accessing URL parameters. 

The component automatically fetches todo details for editing when an ID is detected, and it defines a form that submits these details either to create a new todo or update an existing one. 

The component dynamically adjusts its header to indicate whether it's being used to add or update a todo. The form includes fields for entering the todo's title, description, and completion status, with a submit button that triggers the save or update operation.

Run React App

Hit this URL in the browser: http://localhost:3000

Add Todo Page:

Spring Boot React Project - Add Todo Page

List Todo Page with Update, Delete, Complete, In Complete

Spring Boot React Project - List Todos Page
Update Todo Page:
Spring Boot React Project - Update Todo Page

Source Code on GitHub

The source code for this project is available in my GitHub repository.

Comments