Creating a CRUD Application with Spring Boot and React: A Step-by-Step Guide with MySQL
In modern web development, the combination of a powerful backend framework like Spring Boot and a flexible frontend library like React is a popular choice for building robust, scalable applications. When you add MySQL, one of the world's most popular open-source databases, you have a complete, enterprise-ready technology stack.
This article will walk through the entire process of building a full-stack CRUD (Create, Read, Update, Delete) application. We'll create a Spring Boot backend to manage a list of users by creating a REST API, and connect with a React frontend.
By the end of this tutorial, you will have a practical understanding of the REST api in Spring Boot and how to use the api in our frontend application.
Prerequisites
Before we start, make sure you have the following installed:
- Java Development Kit (JDK).
- Maven or Gradle for dependency management. We'll use Maven in this guide.
- An IDE like IntelliJ IDEA or Eclipse.
- Node.js and npm for managing the React project.
- MySQL Server and a client like MySQL Workbench.
Let's first set up our backend application using Spring Boot.
Part 1: Building the Backend with Spring Boot
First, we'll create the Spring Boot application that we will use to create a REST API.
Step 1: Initialize the Spring Boot Project
The easiest way to start is with the Spring Initializr.
- Go to start.spring.io.
- Configure your project:
- Project: Maven
- Language: Java
- Spring Boot: The latest stable version.
- Group: com.example
- Artifact: crud-backend
- Packaging: Jar
- Java: choose the latest one
- Add the following dependencies:
- Spring Web: For building RESTful web applications.
- Spring Data JPA: To simplify database interactions using ORM.
- MySQL Driver: The JDBC driver that helps to connect to MySQL.
- Lombok: Reduces boilerplate code like getters, setters, and constructors.
- DevTools: For development purposes, to not restart the application after changes to the codebase.
Click "Generate" to download the project zip file. Unzip it and open it in your IDE.
Step 2: Configure the MySQL Database Connection
Open the src/main/resources/application.properties file and add your MySQL database configuration. Create a database named user_management_db in your MySQL server beforehand.
application.properties
# MySQL Database Configuration spring.datasource.url=jdbc:mysql://localhost:3306/user spring.datasource.username=root spring.datasource.password=root # JPA/Hibernate Configuration spring.jpa.hibernate.ddl-auto=update spring.jpa.show-sql=true spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL8Dialect
spring.datasource.url: This tells the database url, and here user is a database name.
spring.datasource.username: username for accessing the database.
spring.datasource.password: passwords for accessing the database.
spring.jpa.hibernate.ddl-auto=update: This tells Hibernate to automatically update the database schema based on your entities. For production, you should use a migration tool like Flyway
Step 3: Create the Model (Entity)
Now, let's define the data model. Create a User class that creates a table of users in the database with the help of Hibernate.
src/main/java/com/example/crudbackend/model/User.java
Java
package com.example.crudbackend.model;
import lombok.Data;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Table;
@Entity
@Table(name = "users")
@Data // Lombok annotation for getters, setters, toString, etc.
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String firstName;
private String lastName;
private String email;
}
@Entity: Marks this class as a JPA entity.
@Table(name = "users"): Specifies the table name in the database.
@Id: Designates the id field as the primary key.
@GeneratedValue: Configures the primary key generation strategy. IDENTITY is a good choice for MySQL.
@Data: A Lombok annotation that generates all the boilerplate for us.
Step 4: Build the Repository Interface
The repository is the layer that interacts with the database. Spring Data JPA helps to interact with the database without writing the SQL query.
Create a UserRepository interface that extends JpaRepository.
src/main/java/com/example/crudbackend/repository/UserRepository.java
Java
package com.example.crudbackend.repository; import com.example.crudbackend.model.User; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; @Repository public interface UserRepository extends JpaRepository{ }
JpaRepository<User, Long> gives us all the standard CRUD methods that give default such as findAll, findById, save, and deleteById methods to use.
Step 5: Develop the REST Controller
The controller exposes our CRUD operations as RESTful API endpoints.
Create a UserController class.
src/main/java/com/example/crudbackend/controller/UserController.java
Java
package com.example.crudbackend.controller;
import com.example.crudbackend.model.User;
import com.example.crudbackend.repository.UserRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@CrossOrigin(origins = "http://localhost:3000") // Allow requests from React app
@RestController
@RequestMapping("/api/v1/users")
public class UserController {
@Autowired
private UserRepository userRepository;
// Get all users
@GetMapping
public List getAllUsers() {
return userRepository.findAll();
}
// Create a new user
@PostMapping
public User createUser(@RequestBody User user) {
return userRepository.save(user);
}
// Get a single user by id
@GetMapping("/{id}")
public ResponseEntity getUserById(@PathVariable Long id) {
User user = userRepository.findById(id)
.orElseThrow(() -> new RuntimeException("User not found with id: " + id));
return ResponseEntity.ok(user);
}
// Update a user
@PutMapping("/{id}")
public ResponseEntity updateUser(@PathVariable Long id, @RequestBody User userDetails) {
User user = userRepository.findById(id)
.orElseThrow(() -> new RuntimeException("User not found with id: " + id));
user.setFirstName(userDetails.getFirstName());
user.setLastName(userDetails.getLastName());
user.setEmail(userDetails.getEmail());
User updatedUser = userRepository.save(user);
return ResponseEntity.ok(updatedUser);
}
// Delete a user
@DeleteMapping("/{id}")
public ResponseEntity deleteUser(@PathVariable Long id) {
User user = userRepository.findById(id)
.orElseThrow(() -> new RuntimeException("User not found with id: " + id));
userRepository.delete(user);
return ResponseEntity.noContent().build();
}
}
Key Annotations:
@RestController: Combines @Controller and @ResponseBody, marking this class for RESTful requests.
@RequestMapping("/api/v1/users"): Sets the base path for all endpoints in this controller.
@CrossOrigin(origins = "http://localhost:3000"): This is crucial! It allows our React application (running on port 3000) to make API calls to this backend (running on port 8080), avoiding Cross-Origin Resource Sharing (CORS) errors.
@GetMapping, @PostMapping, @PutMapping, @DeleteMapping: Map HTTP methods to specific controller actions.
Our backend is now ready! You can run it from your IDE or by using the command mvn spring-boot:run.
Part 2: Building the Frontend with React
Now, let's create the frontend where the user can interact with our API.
Step 1: Initialize the React Project
Open a new terminal window and run the following command to create a new React app:
Bash
npx create-react-app crud-frontend cd crud-frontend
Step 2: Install Dependencies
We need Axios to make HTTP requests to our Spring Boot backend and bootstrap for some basic styling.
Bash
npm install axios bootstrap
Import Bootstrap's CSS into your src/index.js file:
JavaScript
import 'bootstrap/dist/css/bootstrap.min.css';
Step 3: Create a Service for API Calls
It's good practice to centralize API logic because whenever we move to the product in that time domain of the backend application or url of the backend can be changed, this centralized file helps by only changing url at the one file not everywhere. Create a new file src/services/UserService.js.
src/services/UserService.js
JavaScript
import axios from 'axios';
const API_BASE_URL = "http://localhost:8080/api/v1/users";
class UserService {
getUsers() {
return axios.get(API_BASE_URL);
}
createUser(user) {
return axios.post(API_BASE_URL, user);
}
getUserById(userId) {
return axios.get(API_BASE_URL + '/' + userId);
}
updateUser(user, userId) {
return axios.put(API_BASE_URL + '/' + userId, user);
}
deleteUser(userId) {
return axios.delete(API_BASE_URL + '/' + userId);
}
}
export default new UserService();
Step 4: Build the Main User Component
Let's create the component that will display the list of users and a form for creating/updating them. Replace the content of src/App.js with the following:
src/App.js
JavaScript
import React, { useState, useEffect } from 'react';
import UserService from './services/UserService';
import './App.css';
function App() {
const [users, setUsers] = useState([]);
const [currentUser, setCurrentUser] = useState({ id: null, firstName: '', lastName: '', email: '' });
const [isEditing, setIsEditing] = useState(false);
useEffect(() => {
loadUsers();
}, []);
const loadUsers = () => {
UserService.getUsers().then(response => {
setUsers(response.data);
});
};
const handleInputChange = (event) => {
const { name, value } = event.target;
setCurrentUser({ ...currentUser, [name]: value });
};
const handleSubmit = (event) => {
event.preventDefault();
if (isEditing) {
UserService.updateUser(currentUser, currentUser.id).then(() => {
loadUsers();
});
} else {
UserService.createUser(currentUser).then(() => {
loadUsers();
});
}
resetForm();
};
const handleEdit = (user) => {
setIsEditing(true);
setCurrentUser(user);
};
const handleDelete = (id) => {
UserService.deleteUser(id).then(() => {
loadUsers();
});
};
const resetForm = () => {
setIsEditing(false);
setCurrentUser({ id: null, firstName: '', lastName: '', email: '' });
};
return (
<div className="container mt-4">
<h1 className="text-center">User Management System</h1>
<div className="row">
<div className="col-md-8">
<h2>User List</h2>
<table className="table table-bordered">
<thead className="thead-dark">
<tr>
<th>First Name</th>
<th>Last Name</th>
<th>Email</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{users.map(user => (
<tr key={user.id}>
<td>{user.firstName}</td>
<td>{user.lastName}</td>
<td>{user.email}</td>
<td>
<button className="btn btn-warning btn-sm me-2" onClick={() => handleEdit(user)}>Edit</button>
<button className="btn btn-danger btn-sm" onClick={() => handleDelete(user.id)}>Delete</button>
</td>
</tr>
))}
</tbody>
</table>
</div>
<div className="col-md-4">
<h2>{isEditing ? 'Edit User' : 'Add User'}</h2>
<form onSubmit={handleSubmit}>
<div className="form-group">
<label>First Name</label>
<input type="text" name="firstName" className="form-control" value={currentUser.firstName} onChange={handleInputChange} required />
</div>
<div className="form-group">
<label>Last Name</label>
<input type="text" name="lastName" className="form-control" value={currentUser.lastName} onChange={handleInputChange} required />
</div>
<div className="form-group">
<label>Email</label>
<input type="email" name="email" className="form-control" value={currentUser.email} onChange={handleInputChange} required />
</div>
<button type="submit" className="btn btn-primary mt-3">{isEditing ? 'Update' : 'Create'}</button>
{isEditing && <button type="button" className="btn btn-secondary mt-3 ms-2" onClick={resetForm}>Cancel</button>}
</form>
</div>
</div>
</div>
);
}
export default App;
Component Breakdown:
useState: We use state to manage users (the list from the API), currentUser (the data in the form), and isEditing (to toggle between create and update mode).
useEffect: The useEffect hook with an empty dependency array [] runs once when the component mounts, calling loadUsers() to fetch the initial data.
Event Handlers: Functions like handleSubmit, handleEdit, and handleDelete use our UserService to communicate with the backend and then refresh the user list.
Part 3: Running the Full-Stack Application
Run the Backend: Make sure your Spring Boot application is running.
Run the Frontend: In your crud-frontend directory, run the command:
Bash
npm start
This will open your React application in a browser, usually at http://localhost:3000.
You should now see the User Management System. You can add, view, edit, and delete users, with all changes being persisted in your MySQL database!
Output:
Conclusion
Finally, you have successfully built a full-stack CRUD application with Spring Boot, React, and MySQL. You've learned how to create a robust REST API, connect it to a database using Spring Data JPA, and build a dynamic, interactive frontend with React Hooks
This powerful tech stack provides a clear and efficient path for developing modern web applications. Happy coding