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.
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
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
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
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;
}
Create Custom Exception - ResourceNotFoundException
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
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. - 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.
Create Service Layer - TodoServiceImpl class
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.
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
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
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
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
Run React App
Hit this URL in the browser: http://localhost:3000
Add Todo Page:
List Todo Page with Update, Delete, Complete, In Complete
Update Todo Page:
Comments
Post a Comment
Leave Comment