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.

  1. Go to start.spring.io.
  2. 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
  3. 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:

You can customize the frontend design by changing the CSS file according to your taste.

create user

View Users



Edit User


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

Next Post Previous Post
No Comment
Add Comment
comment url