In this tutorial, you will learn how to build a full-stack CRUD (Create, Read, Update, Delete) application using Spring Boot 3 for the backend and React for the frontend. We'll also use PostgreSQL as the database and Axios to make HTTP requests.
By the end of this tutorial, you will have a working web application that allows you to manage a list of users (or any data entity) with create, update, delete, and read functionality.
Prerequisites
- Java 17+
- Node.js & npm
- PostgreSQL is installed and running
- IDE (IntelliJ, VS Code, etc.)
1. Setting Up Spring Boot 3 Backend
1.1 Create Spring Boot Project
Use Spring Initializr with the following settings:
- Project: Maven
- Language: Java
- Spring Boot: 3.x.x
- Dependencies: Spring Web, Spring Data JPA, PostgreSQL Driver
Generate and unzip the project, then open it in your IDE.
1.2 Create the PostgreSQL Database
Open your terminal and run the following commands to create a PostgreSQL database:
psql postgres -U djamware
Once inside the PostgreSQL shell, create the database:
CREATE DATABASE spring_restapi;
\q
Replace spring_restapi with your preferred database name if different.
1.3 Define JPA Entity
Create a model directory:
mkdir src/main/java/com/djamware/spring_restapi/models
Create a src/main/java/com/djamware/spring_restapi/models/User.java entity file:
package com.djamware.spring_restapi.models;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
@Entity
@Table(name = "users")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private String email;
// Getters and setters
}
1.4 Create Repository
Create a repository directory:
mkdir src/main/java/com/djamware/spring_restapi/repositories
Create a src/main/java/com/djamware/spring_restapi/repositories/UserRepository.java repository interface file:
package com.djamware.spring_restapi.repositories;
import org.springframework.data.jpa.repository.JpaRepository;
import com.djamware.spring_restapi.models.User;
public interface UserRepository extends JpaRepository<User, Long> {
}
1.5 Create REST Controller
Create a controller directory:
mkdir src/main/java/com/djamware/spring_restapi/controllers
Create a src/main/java/com/djamware/spring_restapi/controllers/UserController.java controller file:
package com.djamware.spring_restapi.controllers;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.djamware.spring_restapi.models.User;
import com.djamware.spring_restapi.repositories.UserRepository;
@RestController
@RequestMapping("/api/users")
@CrossOrigin(origins = "http://localhost:3000")
public class UserController {
private final UserRepository repository;
@Autowired
public UserController(UserRepository repository) {
this.repository = repository;
}
@GetMapping
public List<User> getAll() {
return repository.findAll();
}
@PostMapping
public User create(@RequestBody User user) {
return repository.save(user);
}
@PutMapping("/{id}")
public ResponseEntity<User> update(@PathVariable Long id, @RequestBody User userDetails) {
User user = repository.findById(id).orElseThrow();
user.setName(userDetails.getName());
user.setEmail(userDetails.getEmail());
return ResponseEntity.ok(repository.save(user));
}
@DeleteMapping("/{id}")
public void delete(@PathVariable Long id) {
repository.deleteById(id);
}
}
1.6 Configure Database
In src/main/resources/application.properties:
spring.application.name=spring-restapi
spring.datasource.url=jdbc:postgresql://localhost:5432/spring_restapi
spring.datasource.username=postgres
spring.datasource.password=yourpassword
spring.jpa.hibernate.ddl-auto=update
2. Setting Up React Frontend
2.1 Create React App
npx create-react-app user-crud-app
cd user-crud-app
npm install axios react-router-dom
2.2 Project Structure
src/
components/
UserList.js
UserForm.js
App.js
api.js
2.3 Axios API File
In src/api.js add:
import axios from 'axios';
export default axios.create({
baseURL: 'http://localhost:8080/api/users'
});
2.4 User List Component
In src/components/UserList.js, add:
import React, { useEffect, useState } from 'react';
import api from '../api';
export default function UserList() {
const [users, setUsers] = useState([]);
const fetchUsers = async () => {
const res = await api.get('/');
setUsers(res.data);
};
useEffect(() => {
fetchUsers();
}, []);
const deleteUser = async (id) => {
await api.delete(`/${id}`);
fetchUsers();
};
return (
<div>
<h2>User List</h2>
<ul>
{users.map(user => (
<li key={user.id}>
{user.name} ({user.email})
<button onClick={() => deleteUser(user.id)}>Delete</button>
</li>
))}
</ul>
</div>
);
}
2.5 User Form Component (Add/Edit)
In src/components/UserForm.js, add:
import React, { useState, useEffect } from "react";
import PropTypes from "prop-types";
import api from "../api";
export default function UserForm({ selectedUser, onSuccess }) {
const [name, setName] = useState("");
const [email, setEmail] = useState("");
useEffect(
() => {
if (selectedUser) {
setName(selectedUser.name);
setEmail(selectedUser.email);
} else {
setName("");
setEmail("");
}
},
[selectedUser]
);
const handleSubmit = async e => {
e.preventDefault();
const userData = { name, email };
if (selectedUser) {
await api.put(`/${selectedUser.id}`, userData);
} else {
await api.post("", userData);
}
setName("");
setEmail("");
onSuccess();
};
return (
<form
onSubmit={handleSubmit}
style={{
marginBottom: "20px",
padding: "10px",
border: "1px solid #ccc",
borderRadius: "8px",
width: "300px"
}}
>
<h3>
{selectedUser ? "Edit User" : "Add User"}
</h3>
<div style={{ marginBottom: "10px" }}>
<label htmlFor="name">Name:</label>
<br />
<input
id="name"
type="text"
value={name}
onChange={e => setName(e.target.value)}
required
style={{ width: "100%", padding: "8px" }}
/>
</div>
<div style={{ marginBottom: "10px" }}>
<label htmlFor="email">Email:</label>
<br />
<input
id="email"
type="email"
value={email}
onChange={e => setEmail(e.target.value)}
required
style={{ width: "100%", padding: "8px" }}
/>
</div>
<button type="submit" style={{ padding: "8px 16px" }}>
{selectedUser ? "Update" : "Create"}
</button>
</form>
);
}
UserForm.propTypes = {
selectedUser: PropTypes.shape({
id: PropTypes.number,
name: PropTypes.string,
email: PropTypes.string
}),
onSuccess: PropTypes.func.isRequired
};
2.6 Main Component Integration
Now, create a main component App.js to integrate both UserForm and UserList:
import React, { useEffect, useState } from "react";
import { BrowserRouter as Router, Routes, Route, Link } from "react-router-dom";
import PropTypes from "prop-types";
import UserList from "./components/UserList";
import UserForm from "./components/UserForm";
import api from "./api";
function Home({ users, onEdit, onSuccess }) {
return (
<div>
<UserList users={users} onEdit={onEdit} onSuccess={onSuccess} />
</div>
);
}
Home.propTypes = {
users: PropTypes.array.isRequired,
onEdit: PropTypes.func.isRequired,
onSuccess: PropTypes.func.isRequired
};
function ManageUsers({ selectedUser, onSuccess }) {
return (
<div>
<UserForm selectedUser={selectedUser} onSuccess={onSuccess} />
</div>
);
}
ManageUsers.propTypes = {
selectedUser: PropTypes.object,
onSuccess: PropTypes.func.isRequired
};
function App() {
const [users, setUsers] = useState([]);
const [selectedUser, setSelectedUser] = useState(null);
const fetchUsers = async () => {
const res = await api.get("");
setUsers(res.data);
};
useEffect(() => {
fetchUsers();
}, []);
const handleSuccess = () => {
fetchUsers();
setSelectedUser(null);
};
const handleEdit = user => {
setSelectedUser(user);
};
return (
<Router>
<div style={{ padding: "20px" }}>
<nav style={{ marginBottom: "20px" }}>
<Link to="/" style={{ marginRight: "10px" }}>
Home
</Link>
<Link to="/manage">Add/Edit User</Link>
</nav>
<Routes>
<Route
path="/"
element={
<Home
users={users}
onEdit={handleEdit}
onSuccess={handleSuccess}
/>
}
/>
<Route
path="/manage"
element={
<ManageUsers
selectedUser={selectedUser}
onSuccess={handleSuccess}
/>
}
/>
</Routes>
</div>
</Router>
);
}
export default App;
This wraps up routing and form validation in the React frontend for the Spring Boot full-stack CRUD application.
2.7 Delete Confirmation
To improve UX and prevent accidental deletions, modify the delete action in UserList.js:
import React from 'react';
import PropTypes from 'prop-types';
import api from '../api';
export default function UserList({ users, onEdit, onSuccess }) {
const handleDelete = async (id) => {
if (!window.confirm('Are you sure you want to delete this user?')) return;
try {
await api.delete(`/${id}`);
onSuccess();
} catch (err) {
console.error(err);
}
};
return (
<table>
<thead>
<tr>
<th>Name</th>
<th>Email</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{users.map((user) => (
<tr key={user.id}>
<td>{user.name}</td>
<td>{user.email}</td>
<td>
<button onClick={() => onEdit(user)}>Edit</button>
<button onClick={() => handleDelete(user.id)}>Delete</button>
</td>
</tr>
))}
</tbody>
</table>
);
}
UserList.propTypes = {
users: PropTypes.array.isRequired,
onEdit: PropTypes.func.isRequired,
onSuccess: PropTypes.func.isRequired,
};
2.8 Enable Edit Navigation with useNavigate
Update App.js to allow navigating to the form when a user clicks "Edit". Do not wrap App with Router here, it's already done in index.js:
import { useNavigate } from "react-router-dom";
function App() {
const [users, setUsers] = useState([]);
const [selectedUser, setSelectedUser] = useState(null);
const navigate = useNavigate();
const fetchUsers = async () => {
const res = await api.get("");
setUsers(res.data);
};
useEffect(() => {
fetchUsers();
}, []);
const handleSuccess = () => {
fetchUsers();
setSelectedUser(null);
};
const handleEdit = (user) => {
setSelectedUser(user);
navigate("/manage");
};
return (
<div style={{ padding: "20px" }}>
<nav style={{ marginBottom: "20px" }}>
<Link to="/" style={{ marginRight: "10px" }}>Home</Link>
<Link to="/manage">Add/Edit User</Link>
</nav>
<Routes>
<Route
path="/"
element={<Home users={users} onEdit={handleEdit} onSuccess={handleSuccess} />}
/>
<Route
path="/manage"
element={<ManageUsers selectedUser={selectedUser} onSuccess={handleSuccess} />}
/>
</Routes>
</div>
);
}
Make sure App is rendered inside the <Router> component, typically in index.js:
import React from "react";
import ReactDOM from "react-dom/client";
import { BrowserRouter } from "react-router-dom";
import App from "./App";
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
<BrowserRouter>
<App />
</BrowserRouter>
);
2.9 Fixing CORS Issue in Spring Boot Backend
To allow frontend requests from http://localhost:3000, you must enable CORS in your Spring Boot backend.
Option 1: Global CORS Configuration
Create a config folder and class:
mkdir src/main/java/com/djamware/spring_restapi/config
In src/main/java/com/djamware/spring_restapi/config/WebConfig.java, add:
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class WebConfig {
@Bean
public WebMvcConfigurer corsConfigurer() {
return new WebMvcConfigurer() {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins("http://localhost:3000")
.allowedMethods("GET", "POST", "PUT", "DELETE")
.allowedHeaders("*");
}
};
}
}
Alternatively, you can annotate the controller class or methods with @CrossOrigin(origins = "http://localhost:3000").
2.10 Fixing 404 Error in React Axios Call
If you're getting a 404 when making a GET request to /api/users/, it's likely due to the trailing slash. In your App.js, fix this line:
const res = await api.get(""); // instead of "/"
This avoids issues when Axios baseURL already ends with /api/users.
2.11 Run and Test the Full-Stack App
Start the backend server:
mvn spring-boot:run
Start the frontend React app:
npm start
Visit http://localhost:3000 and test full CRUD functionality:
- Add a user using the "Add/Edit User" form
- View users on the Home page
- Edit a user by clicking the Edit button
- Delete a user with confirmation
2.12 Optional: Deployment
Deploy the Backend with Docker
Create a Dockerfile in your Spring Boot project root:
FROM eclipse-temurin:17-jdk-alpine
VOLUME /tmp
COPY target/*.jar app.jar
ENTRYPOINT ["java","-jar","/app.jar"]
Build and run:
docker build -t springboot3-react-crud-backend .
docker run -p 8080:8080 springboot3-react-crud-backend
Deploy React Frontend
Build the production app:
npm run build
Host it using services like Netlify, Vercel, or static hosting via NGINX.
2.14 Add Form Validation Messages
In UserForm.js, update the form to include basic client-side validation and display error messages when fields are left blank.
Updated UserForm.js:
import React, { useEffect, useState } from "react";
import PropTypes from "prop-types";
import api from "../api";
import { useNavigate } from "react-router-dom";
export default function UserForm({ selectedUser, onSuccess }) {
const [name, setName] = useState("");
const [email, setEmail] = useState("");
const [errors, setErrors] = useState({});
const navigate = useNavigate();
useEffect(() => {
if (selectedUser) {
setName(selectedUser.name);
setEmail(selectedUser.email);
} else {
setName("");
setEmail("");
}
}, [selectedUser]);
const validate = () => {
const newErrors = {};
if (!name.trim()) newErrors.name = "Name is required";
if (!email.trim()) newErrors.email = "Email is required";
else if (!/\S+@\S+\.\S+/.test(email)) newErrors.email = "Invalid email format";
setErrors(newErrors);
return Object.keys(newErrors).length === 0;
};
const handleSubmit = async (e) => {
e.preventDefault();
if (!validate()) return;
const user = { name, email };
try {
if (selectedUser) {
await api.put(`/${selectedUser.id}`, user);
} else {
await api.post("", user);
}
onSuccess();
navigate("/");
} catch (err) {
console.error(err);
}
};
return (
<form onSubmit={handleSubmit}>
<div style={{ marginBottom: "10px" }}>
<label htmlFor="name">Name:</label><br />
<input
id="name"
type="text"
value={name}
onChange={(e) => setName(e.target.value)}
/>
{errors.name && <div style={{ color: "red" }}>{errors.name}</div>}
</div>
<div style={{ marginBottom: "10px" }}>
<label htmlFor="email">Email:</label><br />
<input
id="email"
type="text"
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
{errors.email && <div style={{ color: "red" }}>{errors.email}</div>}
</div>
<button type="submit">
{selectedUser ? "Update" : "Create"} User
</button>
</form>
);
}
UserForm.propTypes = {
selectedUser: PropTypes.object,
onSuccess: PropTypes.func.isRequired,
};
2.15 UI Framework Styling with Bootstrap
Install Bootstrap:
npm install bootstrap
Import it in index.js:
import 'bootstrap/dist/css/bootstrap.min.css';
Then, style components like:
<form onSubmit={handleSubmit} className="container mt-4">
<div className="mb-3">
<label htmlFor="name" className="form-label">Name</label>
<input type="text" className="form-control" value={name} onChange={...} />
{errors.name && <div className="text-danger">{errors.name}</div>}
</div>
...
</form>
This completes the full-stack Spring Boot 3 and React CRUD tutorial with routing, validation, error handling, and styling.
3. Conclusion
In this tutorial, you built a complete full-stack CRUD application using Spring Boot 3 for the backend and React for the frontend. You learned how to:
- Set up RESTful APIs with Spring Boot and connect to a PostgreSQL database
- Perform CRUD operations from a React frontend using Axios
- Navigate between views using React Router
- Validate form inputs and display error messages
- Handle server-side errors gracefully
- Style your application using Bootstrap for a cleaner UI
By combining these tools and practices, you’ve created a solid foundation for developing modern web applications. You can now expand this project by adding features like authentication, pagination, file uploads, or deploying it to the cloud.
You can find the full Spring Boot Backend and React Frontend source code on our GitHub repository.
That just the basic. If you need more deep learning about Java and Spring Framework you can take the following cheap course:
- Java basics, Java in Use //Complete course for beginners
- Java Programming: Master Basic Java Concepts
- Master Java Web Services and REST API with Spring Boot
- JDBC Servlets and JSP - Java Web Development Fundamentals
- The Complete Java Web Development Course
- Spring MVC For Beginners: Build Java Web App in 25 Steps
- Practical RESTful Web Services with Java EE 8 (JAX-RS 2.1)
- Mastering React JS
- Master React Native Animations
- React: React Native Mobile Development: 3-in-1
- MERN Stack Front To Back: Full Stack React, Redux & Node. js
- Learning React Native Development
Thanks!