React CRUD App with a Spring Boot API

Building a React CRUD App with a Spring Boot API

Building a CRUD app is a fundamental exercise in web development. In this article, we’ll explore how to create a CRUD application using React for the front end and a Spring Boot API for the back end, offering a step-by-step approach to integrating these technologies effectively.

Prerequisites

  • Node.js
  • JAVA 17
  • Maven
  • MySQL

Setup React project

npm create vite@4.4.0 view -- --template react
cd view
npm install react-router-dom@5 axios

React project structure

├─ index.html
├─ public
│  └─ css
│     └─ style.css
└─ src
   ├─ components
   │  └─ product
   │     ├─ Create.jsx
   │     ├─ Delete.jsx
   │     ├─ Detail.jsx
   │     ├─ Edit.jsx
   │     ├─ Index.jsx
   │     └─ Service.js
   ├─ history.js
   ├─ http.js
   ├─ main.jsx
   ├─ router.jsx
   └─ App.jsx

React project files

main.jsx

import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App'

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

The main.jsx file is the entry point for a React app. It imports React, ReactDOM, and the App component. The ReactDOM.createRoot method is used to render the App component into the HTML element with the ID root.

App.jsx

import React, { useState, useEffect } from 'react'
import { Router, Link } from 'react-router-dom'
import history from './history'
import Route  from './router'

export default function App() {
  return (
    <Router history={history}>
      <Route />
    </Router>
  )
}

The App.jsx file sets up routing for a React app. The App component wraps the application in a Router using the custom history, and renders the Route component for handling routes.

history.js

import { createBrowserHistory } from 'history'

export default createBrowserHistory()

The history.js file exports a custom browser history object created with createBrowserHistory for managing navigation in a React app.

router.jsx

import React, { Suspense, lazy } from 'react'
import { Switch, Route, Redirect } from 'react-router-dom'

export default function AppRoute(props) {
  return (
    <Suspense fallback={''}>
      <Switch>
        <Route path="/" component={(p) => <Redirect to="/product" /> } exact />
        <Route path="/product" component={lazy(() => import('./components/product/Index'))} exact />
        <Route path="/product/create" component={lazy(() => import('./components/product/Create'))} exact />
        <Route path="/product/:id/" component={lazy(() => import('./components/product/Detail'))} exact />
        <Route path="/product/edit/:id/" component={lazy(() => import('./components/product/Edit'))} exact />
        <Route path="/product/delete/:id/" component={lazy(() => import('./components/product/Delete'))} exact />
      </Switch>
    </Suspense>
  )
}

The router.jsx file sets up routing for a React app with lazy-loaded components. It uses Suspense to handle loading status, and Switch to define routes. The root path redirects to /product, and specific routes handle product-related pages like create, detail, edit, and delete.

http.js

import axios from 'axios'

let http = axios.create({
  baseURL: 'http://localhost:8080/api',
  headers: {
    'Content-type': 'application/json'
  }
})

export default http

The http.js file configures and exports an Axios instance with a centralized base URL, which is a standard practice for managing API endpoints and default headers set to application/json.

Create.jsx

import React, { useState, useEffect } from 'react'
import { Link } from 'react-router-dom'
import Service from './Service'

export default function ProductCreate(props) {
  
  const [ product, setProduct ] = useState({})

  function create(e) {
    e.preventDefault()
    Service.create(product).then(() => {
      props.history.push('/product')
    }).catch((e) => {
      alert(e.response.data)
    })
  }

  function onChange(e) {
    let data = { ...product }
    data[e.target.name] = e.target.value
    setProduct(data)
  }

  return (
    <div className="container">
      <div className="row">
        <div className="col">
          <form method="post" onSubmit={create}>
            <div className="row">
              <div className="mb-3 col-md-6 col-lg-4">
                <label className="form-label" htmlFor="product_name">Name</label>
                <input id="product_name" name="name" className="form-control" onChange={onChange} value={product.name ?? '' } maxLength="50" />
              </div>
              <div className="mb-3 col-md-6 col-lg-4">
                <label className="form-label" htmlFor="product_price">Price</label>
                <input id="product_price" name="price" className="form-control" onChange={onChange} value={product.price ?? '' } type="number" />
              </div>
              <div className="col-12">
                <Link className="btn btn-secondary" to="/product">Cancel</Link>
                <button className="btn btn-primary">Submit</button>
              </div>
            </div>
          </form>
        </div>
      </div>
    </div>
  )
}

The create.jsx file defines a ProductCreate component for adding a new product. It uses useState to manage form data, and handles form submission with create(e) that sends data via Service.create and redirects on success. The form includes fields for product name and price, with a cancel link and submit button.

Delete.jsx

import React, { useState, useEffect } from 'react'
import { Link } from 'react-router-dom'
import Service from './Service'

export default function ProductDelete(props) {
  
  const [ product, setProduct ] = useState({})
  
  useEffect(() => {
    get()
  }, [ props.match.params.id ])
  
  function get() {
    return Service.delete(props.match.params.id).then(response => {
      setProduct(response.data)
    }).catch(e => {
      alert(e.response.data)
    })
  }

  function remove(e) {
    e.preventDefault()
    Service.delete(props.match.params.id, product).then(() => {
      props.history.push('/product')
    }).catch((e) => {
      alert(e.response.data)
    })
  }

  return (
    <div className="container">
      <div className="row">
        <div className="col">
          <form method="post" onSubmit={remove}>
            <div className="row">
              <div className="mb-3 col-md-6 col-lg-4">
                <label className="form-label" htmlFor="product_id">Id</label>
                <input readOnly id="product_id" name="id" className="form-control" value={product.id ?? '' } type="number" required />
              </div>
              <div className="mb-3 col-md-6 col-lg-4">
                <label className="form-label" htmlFor="product_name">Name</label>
                <input readOnly id="product_name" name="name" className="form-control" value={product.name ?? '' } maxLength="50" />
              </div>
              <div className="mb-3 col-md-6 col-lg-4">
                <label className="form-label" htmlFor="product_price">Price</label>
                <input readOnly id="product_price" name="price" className="form-control" value={product.price ?? '' } type="number" />
              </div>
              <div className="col-12">
                <Link className="btn btn-secondary" to="/product">Cancel</Link>
                <button className="btn btn-danger">Delete</button>
              </div>
            </div>
          </form>
        </div>
      </div>
    </div>
  )
}

The Delete.jsx file defines a ProductDelete component for deleting a product. It fetches product details using Service.delete and displays them in a read-only form. The component uses useEffect to load product data based on the product ID from the route parameters. The remove(e) function handles deletion and redirects to /product upon success.

Detail.jsx

import React, { useState, useEffect } from 'react'
import { Link } from 'react-router-dom'
import Service from './Service'

export default function ProductDetail(props) {
  
  const [ product, setProduct ] = useState({})
  
  useEffect(() => {
    get()
  }, [ props.match.params.id ])
  
  function get() {
    return Service.get(props.match.params.id).then(response => {
      setProduct(response.data)
    }).catch(e => {
      alert(e.response.data)
    })
  }

  return (
    <div className="container">
      <div className="row">
        <div className="col">
          <form method="post">
            <div className="row">
              <div className="mb-3 col-md-6 col-lg-4">
                <label className="form-label" htmlFor="product_id">Id</label>
                <input readOnly id="product_id" name="id" className="form-control" value={product.id ?? '' } type="number" required />
              </div>
              <div className="mb-3 col-md-6 col-lg-4">
                <label className="form-label" htmlFor="product_name">Name</label>
                <input readOnly id="product_name" name="name" className="form-control" value={product.name ?? '' } maxLength="50" />
              </div>
              <div className="mb-3 col-md-6 col-lg-4">
                <label className="form-label" htmlFor="product_price">Price</label>
                <input readOnly id="product_price" name="price" className="form-control" value={product.price ?? '' } type="number" />
              </div>
              <div className="col-12">
                <Link className="btn btn-secondary" to="/product">Back</Link>
                <Link className="btn btn-primary" to={`/product/edit/${product.id}`}>Edit</Link>
              </div>
            </div>
          </form>
        </div>
      </div>
    </div>
  )
}

The Detail.jsx file defines a ProductDetail component that displays details of a product. It fetches product data using Service.get based on the product ID from the route parameters and displays it in a read-only form, and provides links to go back to the product list or to edit the product.

Edit.jsx

import React, { useState, useEffect } from 'react'
import { Link } from 'react-router-dom'
import Service from './Service'

export default function ProductEdit(props) {
  
  const [ product, setProduct ] = useState({})
  
  useEffect(() => {
    get()
  }, [ props.match.params.id ])
  
  function get() {
    return Service.edit(props.match.params.id).then(response => {
      setProduct(response.data)
    }).catch(e => {
      alert(e.response.data)
    })
  }

  function edit(e) {
    e.preventDefault()
    Service.edit(props.match.params.id, product).then(() => {
      props.history.push('/product')
    }).catch((e) => {
      alert(e.response.data)
    })
  }

  function onChange(e) {
    let data = { ...product }
    data[e.target.name] = e.target.value
    setProduct(data)
  }

  return (
    <div className="container">
      <div className="row">
        <div className="col">
          <form method="post" onSubmit={edit}>
            <div className="row">
              <div className="mb-3 col-md-6 col-lg-4">
                <label className="form-label" htmlFor="product_id">Id</label>
                <input readOnly id="product_id" name="id" className="form-control" onChange={onChange} value={product.id ?? '' } type="number" required />
              </div>
              <div className="mb-3 col-md-6 col-lg-4">
                <label className="form-label" htmlFor="product_name">Name</label>
                <input id="product_name" name="name" className="form-control" onChange={onChange} value={product.name ?? '' } maxLength="50" />
              </div>
              <div className="mb-3 col-md-6 col-lg-4">
                <label className="form-label" htmlFor="product_price">Price</label>
                <input id="product_price" name="price" className="form-control" onChange={onChange} value={product.price ?? '' } type="number" />
              </div>
              <div className="col-12">
                <Link className="btn btn-secondary" to="/product">Cancel</Link>
                <button className="btn btn-primary">Submit</button>
              </div>
            </div>
          </form>
        </div>
      </div>
    </div>
  )
}

The Edit.jsx file defines a ProductEdit component for updating product details. It fetches the current product data using Service.edit and populates a form with this data. The form allows users to modify the product's name and price. On form submission, the edit(e) function updates the product via Service.edit and redirects to the product list on success.

Index.jsx

import React, { useState, useEffect } from 'react'
import { Link } from 'react-router-dom'
import Service from './Service'

export default function ProductIndex(props) {

  const [products, setProducts] = useState([])

  useEffect(() => {
    get()
  }, [props.location])

  function get() {
    Service.get().then(response => {
      setProducts(response.data)
    }).catch(e => {
      alert(e.response.data)
    })
  }

  return (
    <div className="container">
      <div className="row">
        <div className="col">
          <table className="table table-striped table-hover">
            <thead>
              <tr>
                <th>Id</th>
                <th>Name</th>
                <th>Price</th>
                <th>Actions</th>
              </tr>
            </thead>
            <tbody>
              {products.map((product, index) =>
              <tr key={index}>
                <td className="text-center">{product.id}</td>
                <td>{product.name}</td>
                <td className="text-center">{product.price}</td>
                <td className="text-center">
                  <Link className="btn btn-secondary" to={`/product/${product.id}`} title="View"><i className="fa fa-eye"></i></Link>
                  <Link className="btn btn-primary" to={`/product/edit/${product.id}`} title="Edit"><i className="fa fa-pencil"></i></Link>
                  <Link className="btn btn-danger" to={`/product/delete/${product.id}`} title="Delete"><i className="fa fa-times"></i></Link>
                </td>
              </tr>
              )}
            </tbody>
          </table>
          <Link className="btn btn-primary" to="/product/create">Create</Link>
        </div>
      </div>
    </div>
  )
}

The Index.jsx file defines a ProductIndex component that displays a list of products in a table. It fetches product data using Service.get and updates the list on component mount. The table shows product ID, name, and price, with action buttons for viewing, editing, and deleting each product. It also includes a link to create a new product.

Service.js

import http from '../../http'

export default {
  get(id) {
    if (id) {
      return http.get(`/products/${id}`)
    }
    else {
      return http.get('/products' + location.search)
    }
  },
  create(data) {
    if (data) {
      return http.post('/products', data)
    }
    else {
      return http.get('/products/create')
    }
  },
  edit(id, data) {
    if (data) {
      return http.put(`/products/${id}`, data)
    }
    else {
      return http.get(`/products/${id}`)
    }
  },
  delete(id, data) {
    if (data) {
      return http.delete(`/products/${id}`)
    }
    else {
      return http.get(`/products/${id}`)
    }
  }
}

The Service.js file defines API methods for handling product operations. It uses an http instance for making requests:

  • get(id) Retrieves a single product by ID or all products if no ID is provided.
  • create(data) Creates a new product with the provided data or fetches the creation form if no data is provided.
  • edit(id, data) Updates a product by ID with the provided data or fetches the product details if no data is provided.
  • delete(id, data) Deletes a product by ID or fetches the product details if no data is provided.

style.css

.container {
  margin-top: 2em;
}

.btn {
  margin-right: 0.25em;
}

The CSS adjusts the layout by adding space above the container and spacing out buttons horizontally.

index.html

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width,initial-scale=1">
  <link href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.3/css/bootstrap.min.css" rel="stylesheet">
  <link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css" rel="stylesheet">
  <link href="/css/style.css" rel="stylesheet">
</head>
<body>
  <div id="root"></div>
  <script type="module" src="/src/main.jsx"></script>
</body>
</html>

The HTML serves as the main entry point for an React application, including Bootstrap for styling, Font Awesome for icons, It features a div with the ID root where the React app will render.

Setup Spring Boot API project

Create a testing database named "example" and execute the database.sql file to import the table and data.

Spring Boot API Project structure

├─ pom.xml
└─ src
   └─ main
      ├─ java
      │  └─ com
      │     └─ stackpuz
      │        └─ example
      │           ├─ App.java
      │           ├─ controller
      │           │  └─ ProductController.java
      │           ├─ entity
      │           │  └─ Product.java
      │           ├─ repository
      │           │  └─ ProductRepository.java
      │           └─ service
      │              └─ ProductService.java
      └─ resources
         └─ application.properties

Spring Boot API Project files

pom.xml

This pom.xml file defines the configuration and dependencies for the Maven project.

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>
	<groupId>com.stackpuz</groupId>
	<artifactId>example-crud</artifactId>
	<version>0.0.1-SNAPSHOT</version>
	<name>example-crud</name>
	<parent>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-parent</artifactId>
		<version>3.0.10</version>
	</parent>
	<dependencies>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-data-jpa</artifactId>
		</dependency>
		<dependency>
	        <groupId>org.springframework.boot</groupId>
	        <artifactId>spring-boot-devtools</artifactId>
	    </dependency>
		<dependency>
			<groupId>mysql</groupId>
			<artifactId>mysql-connector-java</artifactId>
			<version>8.0.30</version>
		</dependency>
		<dependency>
			<groupId>org.projectlombok</groupId>
			<artifactId>lombok</artifactId>
		</dependency>
	</dependencies>
</project>

application.properties

spring.datasource.url= jdbc:mysql://localhost/example
spring.datasource.username = root
spring.datasource.password = 
spring.jpa.properties.hibernate.dialect = org.hibernate.dialect.MySQLDialect

This is the Spring Boot application configuration file, which includes the database connection details.

App.java

package com.stackpuz.example;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class App {

	public static void main(String[] args) {
		SpringApplication.run(App.class, args);
	}

}

This App.java file serves as the main entry point for the Spring Boot application.

ProductRepository.java

package com.stackpuz.example.repository;

import com.stackpuz.example.entity.Product;
import org.springframework.data.jpa.repository.JpaRepository;

public interface ProductRepository extends JpaRepository<Product, Integer> {
    
}

The ProductRepository.java file defines a Spring Data JPA repository interface for the Product entity. It extends JpaRepository, providing CRUD operations and additional query methods for Product entities.

Product.java

package com.stackpuz.example.entity;

import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.*;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

import java.math.BigDecimal;

@Entity
@Getter
@Setter
@NoArgsConstructor
public class Product {

    @Id
    @GeneratedValue(strategy=GenerationType.IDENTITY)
    private int id;
    private String name;
    private BigDecimal price;
}

The Product.java file defines a JPA entity for the Product class in a Spring Boot application. Lombok annotations @Getter, @Setter, and @NoArgsConstructor are used to automatically generate getter and setter methods, and a no-argument constructor.

ProductService.java

package com.stackpuz.example.service;

import com.stackpuz.example.entity.Product;
import com.stackpuz.example.repository.ProductRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.List;

@Service
public class ProductService {

    @Autowired
    private ProductRepository repository;

    public Product saveProduct(Product product) {
        return repository.save(product);
    }

    public List<Product> getProducts() {
        return repository.findAll();
    }

    public Product getProductById(int id) {
        return repository.findById(id).get();
    }

    public Product updateProduct(int id, Product product) {
        Product existing = repository.findById(id).get();
        existing.setName(product.getName());
        existing.setPrice(product.getPrice());
        return repository.save(existing);
    }

    public void deleteProduct(int id) {
        repository.deleteById(id);
    }
}

The ProductService.java file provides a service class for managing Product entities in a Spring Boot application. It uses the ProductRepository to perform CRUD operations. The @Service annotation marks the class as a Spring service component, and @Autowired injects the ProductRepository dependency.

ProductController.java

package com.stackpuz.example.controller;

import com.stackpuz.example.entity.Product;
import com.stackpuz.example.service.ProductService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

import java.util.List;

@RestController
@CrossOrigin(origins = "*", allowedHeaders = "*")
public class ProductController {

    @Autowired
    private ProductService service;

    @GetMapping("/api/products")
    public List<Product> getProducts() {
        return service.getProducts();
    }

    @GetMapping("/api/products/{id}")
    public Product getProduct(@PathVariable int id) {
        return service.getProductById(id);
    }

    @PostMapping("/api/products")
    public Product createProduct(@RequestBody Product product) {
        return service.saveProduct(product);
    }

    @PutMapping("/api/products/{id}")
    public Product updateProduct(@PathVariable int id, @RequestBody Product product) {
        return service.updateProduct(id, product);
    }

    @DeleteMapping("/api/products/{id}")
    public void deleteProduct(@PathVariable int id) {
        service.deleteProduct(id);
    }
}

The ProductController.java file defines a Spring Boot REST controller for managing Product entities. It handles HTTP requests related to products with the following methods:

  • getProducts (GET) retrieves all products.
  • getProduct (GET) fetches a specific product by ID.
  • createProduct (POST) adds a new product.
  • updateProduct (PUT) modifies an existing product by ID.
  • deleteProduct (DELETE) removes a product by ID.

Run projects

Run React project

npm run dev

Run Spring Boot API project

mvn spring-boot:run

Open the web browser and goto http://localhost:5173
You will find this product list page.

list page

Testing

Click the "View" button to see the product details page.

details page

Click the "Edit" button to modify the product and update its details.

edit page

Click the "Submit" button to save the updated product details.

updated data

Click the "Create" button to add a new product and input its details.

create page

Click the "Submit" button to save the new product.

created product

Click the "Delete" button to remove the previously created product.

delete page

Click the "Delete" button to confirm the removal of this product.

deleted product

Conclusion

In conclusion, we have learned how to create a basic React project using JSX syntax to build views and routing, while setting up a Spring Boot API as the backend. By utilizing Spring Data JPA for database operations, we've developed a dynamic front-end that integrates seamlessly with a robust backend, establishing a solid foundation for building modern, full-stack web applications.

Source code: https://github.com/stackpuz/Example-CRUD-React-18-Spring-Boot-3

Create a React CRUD App in Minutes: https://stackpuz.com

Related post