Full-Stack CRUD Web Application with Java, Spring Boot, React JS, and MySQL

In this comprehensive tutorial, we’ll create a full-stack CRUD Todo application using Java, Spring Boot, JavaScript, React JS, and a MySQL database. This guide will walk you through building the backend REST APIs with Spring Boot and the frontend React application to consume these APIs.

Spring Boot React Project

Create a Spring Boot Project

Using Spring Initializr

First, create a Spring Boot project using Spring Initializr. Set up your project with the necessary dependencies, including Spring Web, Spring Data JPA, MySQL Driver, and Lombok.

Add Maven Dependencies

Update your pom.xml file with the following dependencies:

<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>
<dependency>
    <groupId>org.modelmapper</groupId>
    <artifactId>modelmapper</artifactId>
    <version>3.1.1</version>
</dependency>

Explanation:

  • spring-boot-starter-data-jpa: Starter for using Spring Data JPA with Hibernate.
  • spring-boot-starter-web: Starter for building web, including RESTful, applications using Spring MVC.
  • mysql-connector-j: MySQL JDBC driver.
  • lombok: Java library that automatically plugs into your editor and build tools, spicing up your Java.
  • spring-boot-starter-test: Starter for testing Spring Boot applications.
  • modelmapper: Simplifies the mapping of objects.

Configure MySQL Database

Add your MySQL database configuration to the application.properties file:

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

Explanation:

  • spring.datasource.url: JDBC URL for the MySQL database.
  • spring.datasource.username: Database username.
  • spring.datasource.password: Database password.
  • spring.jpa.properties.hibernate.dialect: Hibernate dialect for MySQL.
  • spring.jpa.hibernate.ddl-auto: Automatically create/update database tables based on entity classes.

Create the Todo Entity

Create a Todo JPA entity:

package net.javaguides.todo.entity;

import jakarta.persistence.*;
import lombok.*;

@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;
}

Explanation:

  • @Entity: Specifies that the class is an entity and is mapped to a database table.
  • @Table(name = "todos"): Specifies the table name in the database.
  • @Id: Specifies the primary key of an entity.
  • @GeneratedValue(strategy = GenerationType.IDENTITY): Provides the specification of generation strategies for the values of primary keys.
  • @Column(nullable = false): Specifies the details of the column to which a field or property will be mapped. nullable = false means the column will not accept null values.
  • @Setter, @Getter, @NoArgsConstructor, @AllArgsConstructor: Lombok annotations to generate boilerplate code like getters, setters, and constructors.

Create TodoRepository

Create the TodoRepository interface:

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> {
}

Explanation:

  • JpaRepository<Todo, Long>: Interface for generic CRUD operations on a repository for a specific type. Here, Todo is the entity type and Long is the type of its primary key.

Create TodoDto

Create a TodoDto class:

package net.javaguides.todo.dto;

import lombok.*;

@Setter
@Getter
@NoArgsConstructor
@AllArgsConstructor
public class TodoDto {
    private Long id;
    private String title;
    private String description;
    private boolean completed;
}

Explanation:

  • This DTO (Data Transfer Object) is used to transfer data between the client and server without exposing the entity directly.

Create Custom Exception – ResourceNotFoundException

Create a ResourceNotFoundException class:

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);
    }
}

Explanation:

  • @ResponseStatus(value = HttpStatus.NOT_FOUND): Marks the exception with a response status code of 404 Not Found.

Create Service Layer – TodoService Interface

Create the TodoService interface:

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);
}

Explanation:

  • Defines the contract for the service layer with methods for CRUD operations and additional operations like marking a todo as complete or incomplete.

Implement Service Layer – TodoServiceImpl

Implement the TodoService interface:

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) {
        Todo todo = modelMapper.map(todoDto, Todo.class);
        Todo savedTodo = todoRepository.save(todo);
        return modelMapper.map(savedTodo, TodoDto.class);
    }

    @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(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(false);
        Todo updatedTodo = todoRepository.save(todo);
        return modelMapper.map(updatedTodo, TodoDto.class);
    }
}

Explanation:

  • TodoServiceImpl implements the TodoService interface and is annotated with @Service, indicating that it is a service component in Spring.
  • Uses ModelMapper to convert between Todo entities and TodoDto objects.
  • Handles CRUD operations and marks todos as complete/incomplete. If an entity is not found, it throws ResourceNotFoundException.

Create REST Controller – TodoController

Create the TodoController class:

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;

    @PostMapping
    public ResponseEntity<TodoDto> addTodo(@RequestBody TodoDto todoDto) {
        TodoDto savedTodo = todoService.addTodo(todoDto);
        return new ResponseEntity<>(savedTodo, HttpStatus.CREATED);
    }

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

    @GetMapping
    public ResponseEntity<List<TodoDto>> getAllTodos() {
        List<TodoDto> todos = todoService.getAllTodos();
        return ResponseEntity.ok(todos);
    }

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

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

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

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

Explanation:

  • @RestController: Indicates that the class is a REST controller.
  • @RequestMapping("api/todos"): Maps HTTP requests to /api/todos.
  • @CrossOrigin("*"): Allows cross-origin requests from any domain.
  • Defines endpoints for adding, retrieving, updating, deleting, and marking todos as complete/incomplete.

Create React App

Setting Up the React Application

Run the following command to create a new React app using Vite:

npm create vite@latest todo-ui

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

Add the following code to the src/main.js file:

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. Install Axios using the following NPM command:

npm add axios --save

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 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');

Explanation:

  • This JavaScript module contains a set of API utility functions using Axios to make HTTP requests to the backend server and manage todo items. These functions facilitate operations corresponding to RESTful services like retrieving, saving, updating, deleting, and marking todos as complete or incomplete.

Configure Routing in React App

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

npm install react-router-dom --save

Open the App.jsx file and add the following content to it:

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

function App() {
  return (
    <>
      <BrowserRouter>
        <HeaderComponent />
        <Routes>
          <Route path='/' element={<ListTodoComponent />} />
          <Route path='/todos' element={<ListTodoComponent />} />
          <Route path='/add-todo' element={<TodoComponent />} />
          <Route path='/update-todo/:id' element={<TodoComponent />} />
        </Routes>
        <FooterComponent />
      </BrowserRouter>
    </>
  );
}

export default App;

Explanation:

  • BrowserRouter: Provides routing context to the nested components.
  • Routes and Route: Define the navigation routes for the application.
  • Static components like HeaderComponent and FooterComponent are always visible.
  • Different paths render different components (ListTodoComponent for listing todos, TodoComponent for adding or updating todos).

Create React Components

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();
  }, []);

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

  const addNewTodo = () => {
    navigate('/add-todo');
  };

  const updateTodo = (id) => {
    navigate(`/update-todo/${id}`);
  };

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

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

  const 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;

Explanation:

  • Manages the display and operations of todo items.
  • Fetches the list of todos on component mount.
  • Provides functions for adding, updating, deleting, and marking todos as complete or incomplete.
  • Renders a table with buttons for each action.

HeaderComponent

import React from 'react';

const HeaderComponent = () => {
  return (
    <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>
  );
};

export default HeaderComponent;

Explanation:

  • Renders the application’s header with a navigation bar.

FooterComponent

import React from 'react';

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

export default FooterComponent;

Explanation:

  • Renders the application’s footer.

TodoComponent

import React, { useEffect, 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();

  const saveOrUpdateTodo = (e) => {
    e.preventDefault();
    const todo = { title, description, completed };

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

  const pageTitle = () => {
    return id ? <h2 className='text-center'>Update Todo</h2> : <h2 className='text-center'>Add Todo</h2>;
  };

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

  return (
    <div className='container'>
      <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)}
                />
              </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)}
                />
              </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={saveOrUpdateTodo}>Submit</button>
            </form>
          </div>
        </div>
      </div>
    </div>
  );
};

export default TodoComponent;

Explanation:

  • Handles both adding new todos and updating existing ones.
  • Initializes state variables for title, description, and completed.
  • Fetches todo details if an ID is present, allowing for editing.
  • Displays a form with fields for entering the todo’s title, description, and completion status, along with a submit button to save or update the todo.

Run React App

Run the React application by hitting the following URL in your 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.

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top