Now, lets configure our Frontend as in previous workshop, we mostly focused on Backend. here, we will concentrate more on Frontend and how it is done.
Let us add functionalities - Clear explanation of code would be given in the video
let us add
Frontend
src/context/CartContext.jsx
import { createContext, useContext, useState } from "react";
const CartContext = createContext();
export function CartProvider({ children }) {
const [cartItems, setCartItems] = useState([]);
const addToCart = (product) => {
setCartItems((currentItems) => {
const existingItem = currentItems.find(
item => item.id === product.id
);
if (existingItem) {
return currentItems.map(item =>
item.id === product.id
? {
...item,
quantity: item.quantity + 1
}
: item
);
}
return [
...currentItems,
{
...product,
quantity: 1
}
];
});
};
const removeFromCart = (productId) => {
setCartItems(currentItems =>
currentItems.filter(item => item.id !== productId)
);
};
const increaseQuantity = (productId) => {
setCartItems(currentItems =>
currentItems.map(item =>
item.id === productId
? {
...item,
quantity: item.quantity + 1
}
: item
)
);
};
const decreaseQuantity = (productId) => {
setCartItems(currentItems =>
currentItems
.map(item =>
item.id === productId
? {
...item,
quantity: item.quantity - 1
}
: item
)
.filter(item => item.quantity > 0)
);
};
return (
{children}
);
}
export function useCart() {
return useContext(CartContext);
}
let us add components like Navigationbar and ProductCard
src/components/Navbar.jsx
import { Link } from "react-router-dom";
import { useCart } from "../context/CartContext";
function Navbar() {
const { cartItems } = useCart();
const cartCount = cartItems.reduce(
(total, item) => total + item.quantity,
0
);
return (
<header className="navbar">
<div className="navbar-container">
<Link to="/" className="navbar-logo">
MAAYA STORE
</Link>
<nav className="navbar-links">
<Link to="/">
Products
</Link>
<Link to="/cart">
🛒 Cart ({cartCount})
</Link>
</nav>
</div>
</header>
);
}
export default Navbar;
src/components/ProductCard.jsx
import { useNavigate } from "react-router-dom";
import { useCart } from "../context/CartContext";
function ProductCard({ product }) {
const navigate = useNavigate();
const { addToCart } = useCart();
const openProduct = () => {
navigate(`/products/${product.id}`);
};
const handleAddToCart = (event) => {
event.stopPropagation();
addToCart(product);
};
return (
<div
className="product-card"
onClick={openProduct}
>
<div className="product-image-container">
<img
src={`http://localhost:8080${product.imagePath}`}
alt={product.name}
className="product-image"
/>
</div>
<div className="product-info">
<h3>{product.name}</h3>
<p className="product-description">
{product.description}
</p>
<p className="product-price">
₹{product.price}
</p>
<p className="product-stock">
Stock: {product.stock}
</p>
<button
className="add-to-cart-button"
onClick={handleAddToCart}
>
Add to Cart
</button>
</div>
</div>
);
}
export default ProductCard;
Create pages for Product and Cart
src/pages/ProductDetails.jsx
import { useEffect, useState } from "react";
import { useNavigate, useParams } from "react-router-dom";
import { useCart } from "../context/CartContext";
function ProductDetails() {
const { id } = useParams();
const navigate = useNavigate();
const { addToCart } = useCart();
const [product, setProduct] = useState(null);
const [error, setError] = useState("");
useEffect(() => {
fetch(`http://localhost:8080/api/products/${id}`)
.then(response => {
if (!response.ok) {
throw new Error("Product not found");
}
return response.json();
})
.then(data => {
setProduct(data);
})
.catch(error => {
console.error(error);
setError("Product not found");
});
}, [id]);
// Product not found
if (error) {
return (
<div className="product-details">
<div className="product-details-content">
<h2>{error}</h2>
<button
className="back-button"
onClick={() => navigate("/")}
>
← Back to Products
</button>
</div>
</div>
);
}
// Loading
if (!product) {
return (
<div className="product-details">
<div className="product-details-content">
<h2>Loading product...</h2>
</div>
</div>
);
}
// Product found
return (
<div className="product-details">
<div className="product-details-content">
<button
className="back-button"
onClick={() => navigate("/")}
>
← Back to Products
</button>
<div className="product-details-container">
{/* Product Image */}
<div className="product-details-image">
<img
src={`http://localhost:8080${product.imagePath}`}
alt={product.name}
/>
</div>
{/* Product Information */}
<div className="product-details-info">
<h1>{product.name}</h1>
<p className="product-details-description">
{product.description}
</p>
<p className="product-details-price">
₹{product.price}
</p>
<p>
Stock: {product.stock}
</p>
<button
className="add-to-cart-button"
onClick={() => {
addToCart(product);
}}
>
Add to Cart
</button>
</div>
</div>
</div>
</div>
);
}
export default ProductDetails;
Cart.jsx
import { useCart } from "../context/CartContext";
function Cart() {
const {
cartItems,
removeFromCart,
increaseQuantity,
decreaseQuantity
} = useCart();
const total = cartItems.reduce(
(sum, item) => sum + item.price * item.quantity,
0
);
return (
<div className="cart-page">
<h1>Your Cart</h1>
{cartItems.length === 0 ? (
<p>Your cart is empty.</p>
) : (
<>
{cartItems.map(item => (
<div
className="cart-item"
key={item.id}
>
<img
src={`http://localhost:8080${item.imagePath}`}
alt={item.name}
/>
<div>
<h3>{item.name}</h3>
<p>
₹{item.price}
</p>
<div>
<button
onClick={() =>
decreaseQuantity(item.id)
}
>
-
</button>
<span>
{" "}
{item.quantity}
{" "}
</span>
<button
onClick={() =>
increaseQuantity(item.id)
}
>
+
</button>
</div>
<button
onClick={() =>
removeFromCart(item.id)
}
>
Remove
</button>
</div>
</div>
))}
<h2>
Total: ₹{total}
</h2>
</>
)}
</div>
);
}
export default Cart;
update index.css
* {
box-sizing: border-box;
}
body {
margin: 0;
font-family: Arial, sans-serif;
background: #f5f5f5;
color: #222;
}
.app {
min-height: 100vh;
}
/* Header */
.header {
background: #04243D;
color: white;
padding: 20px 40px;
}
.header h1 {
margin: 0;
font-size: 28px;
}
/* Main */
.main-content {
max-width: 1200px;
margin: 0 auto;
padding: 40px 20px;
}
.main-content h2 {
text-align: center;
margin-bottom: 35px;
font-size: 32px;
}
/* Product Grid */
.product-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 25px;
}
/* Product Card */
.product-card {
background: white;
border-radius: 12px;
overflow: hidden;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
}
/* Product Image */
.product-image-container {
width: 100%;
height: 280px;
background: #f8f8f8;
display: flex;
align-items: center;
justify-content: center;
}
.product-image {
width: 100%;
height: 100%;
object-fit: contain;
}
/* Product Information */
.product-info {
padding: 20px;
}
.product-info h3 {
margin-top: 0;
font-size: 20px;
}
.product-description {
color: #666;
min-height: 40px;
}
.product-price {
font-size: 22px;
font-weight: bold;
margin: 15px 0 5px;
}
.product-stock {
color: #555;
}
/* Button */
.add-to-cart-button {
width: 100%;
padding: 12px;
border: none;
border-radius: 6px;
background: #5354FF;
color: white;
font-size: 16px;
cursor: pointer;
}
.add-to-cart-button:hover {
opacity: 0.9;
}
/* Responsive */
@media (max-width: 900px) {
.product-grid {
grid-template-columns: repeat(2, 1fr);
}
}
@media (max-width: 600px) {
.product-grid {
grid-template-columns: 1fr;
}
}
/* Product Details */
.product-details {
min-height: 100vh;
padding: 30px;
background: #f5f5f5;
}
.back-button {
border: none;
background: transparent;
font-size: 16px;
cursor: pointer;
margin-bottom: 25px;
}
.product-details-container {
max-width: 1100px;
margin: 0 auto;
background: white;
border-radius: 12px;
padding: 40px;
display: grid;
grid-template-columns: 1fr 1fr;
gap: 50px;
}
.product-details-image {
height: 500px;
background: #f8f8f8;
display: flex;
align-items: center;
justify-content: center;
border-radius: 10px;
}
.product-details-image img {
width: 100%;
height: 100%;
object-fit: contain;
}
.product-details-info {
display: flex;
flex-direction: column;
justify-content: center;
}
.product-details-info h1 {
font-size: 36px;
margin-bottom: 15px;
}
.product-details-description {
color: #666;
font-size: 18px;
line-height: 1.6;
}
.product-details-price {
font-size: 30px;
font-weight: bold;
margin: 25px 0 10px;
}
.product-details-info .add-to-cart-button {
margin-top: 25px;
max-width: 300px;
}
@media (max-width: 700px) {
.product-details-container {
grid-template-columns: 1fr;
padding: 20px;
}
.product-details-image {
height: 350px;
}
}
.product-card {
cursor: pointer;
}
.product-card:hover {
transform: translateY(-4px);
transition: 0.2s;
}
/* =========================
NAVBAR
========================= */
.navbar {
width: 100%;
background-color: #04243D;
color: white;
}
.navbar-container {
max-width: 1200px;
margin: 0 auto;
padding: 18px 30px;
display: flex;
align-items: center;
justify-content: space-between;
}
.navbar-logo {
color: white;
text-decoration: none;
font-size: 24px;
font-weight: 700;
}
.navbar-links {
display: flex;
align-items: center;
gap: 30px;
}
.navbar-links a {
color: white;
text-decoration: none;
font-size: 16px;
}
.navbar-links a:hover {
opacity: 0.8;
}
update App.jsx
import { useEffect, useState } from "react";
import { Routes, Route } from "react-router-dom";
import ProductCard from "./components/ProductCard";
import ProductDetails from "./pages/ProductDetails";
import Cart from "./pages/Cart";
import Navbar from "./components/Navbar";
function Home() {
const [products, setProducts] = useState([]);
useEffect(() => {
fetch("http://localhost:8080/api/products")
.then(response => response.json())
.then(data => {
setProducts(data);
})
.catch(error => {
console.error("Error fetching products:", error);
});
}, []);
return (
<main className="main-content">
<h2>Our Products</h2>
<div className="product-grid">
{products.map(product => (
<ProductCard
key={product.id}
product={product}
/>
))}
</div>
</main>
);
}
function App() {
return (
<div className="app">
<Navbar />
<Routes>
<Route
path="/"
element={<Home />}
/>
<Route
path="/products/:id"
element={<ProductDetails />}
/>
<Route
path="/cart"
element={<Cart />}
/>
</Routes>
</div>
);
}
export default App;
update main.jsx
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import { BrowserRouter } from "react-router-dom";
import "./index.css";
import App from "./App.jsx";
import { CartProvider } from "./context/CartContext";
createRoot(document.getElementById("root")).render(
<StrictMode>
<BrowserRouter>
<CartProvider>
<App />
</CartProvider>
</BrowserRouter>
</StrictMode>
);
Now when you try to click on any product, it does now show product details of that product even though we specifically created seperate page called ProductDetails.
This is because in the backend we did not allow specific Product Get method. Now let us configure it.
Add the following lines in ProductController.java
@GetMapping("/{id}")
public Product getProductById(@PathVariable Long id) {
return productService.getProductById(id);
}
And now add the following lines in ProductService.Java
public Product getProductById(Long id) {
return productRepository.findById(id)
.orElseThrow(() -> new RuntimeException("Product not found"));
}
}
Now rebuild both frontend and backend with respective commands.
Now, if we check our productDetails page. we will have
you will have the product displayed.
Creating User Registration for Authentication.
Now let us create most Important for any website Users Data for Authentication.
create src/main/java/com/maayastore/backend/entity/User.java
package com.maayastore.backend.entity;
import jakarta.persistence.Column;
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;
@Column(unique = true, nullable = false)
private String email;
@Column(nullable = false)
private String password;
private String role;
public User() {
}
public User(String name, String email, String password, String role) {
this.name = name;
this.email = email;
this.password = password;
this.role = role;
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public String getRole() {
return role;
}
public void setRole(String role) {
this.role = role;
}
}
mvn spring-boot:run
psql -d maaya_store_database
\dt
create UserRepository.java
package com.maayastore.backend.repository;
import java.util.Optional;
import org.springframework.data.jpa.repository.JpaRepository;
import com.maayastore.backend.entity.User;
public interface UserRepository extends JpaRepository<User, Long> {
Optional<User> findByEmail(String email);
}
src/main/java/com/maayastore/backend/service/UserService.java
package com.maayastore.backend.service;
import org.springframework.stereotype.Service;
import com.maayastore.backend.entity.User;
import com.maayastore.backend.repository.UserRepository;
@Service
public class UserService {
private final UserRepository userRepository;
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
public User registerUser(User user) {
return userRepository.save(user);
}
}
backend/src/main/java/com/maayastore/backend/controller/AuthController.java
package com.maayastore.backend.controller;
import org.springframework.web.bind.annotation.*;
import com.maayastore.backend.entity.User;
import com.maayastore.backend.service.UserService;
@RestController
@RequestMapping("/api/auth")
public class AuthController {
private final UserService userService;
public AuthController(UserService userService) {
this.userService = userService;
}
@PostMapping("/register")
public User registerUser(@RequestBody User user) {
return userService.registerUser(user);
}
}
Now test it using postman
{
"name": "Sai",
"email": "sai@example.com",
"password": "password123",
"role": "USER"
}
Encrypting password
Now we will encrypt password, add dependency in pom.xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
create backend/src/main/java/com/maaystore/backend/config/SecurityConfig.java
package com.maayastore.backend.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
@Configuration
public class SecurityConfig {
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.csrf(csrf -> csrf.disable())
.authorizeHttpRequests(auth -> auth
.requestMatchers(
"/api/auth/**",
"/api/products/**",
"/images/**"
).permitAll()
.anyRequest().authenticated()
);
return http.build();
}
}
Update userservice.java
package com.maayastore.backend.service;
import org.springframework.security.crypto.password.PasswordEncoder; // NEW LINE ADDED: Imports PasswordEncoder so we can encrypt the user's password.
import org.springframework.stereotype.Service;
import com.maayastore.backend.entity.User;
import com.maayastore.backend.repository.UserRepository;
@Service
public class UserService {
private final UserRepository userRepository;
private final PasswordEncoder passwordEncoder;
// NEW LINE ADDED: Creates a reference to PasswordEncoder.
// We will use this to encrypt the user's password before saving it.
public UserService(
UserRepository userRepository,
PasswordEncoder passwordEncoder) {
// CHANGED: Constructor now receives PasswordEncoder in addition to UserRepository.
// Spring will automatically provide both objects to this constructor.
this.userRepository = userRepository;
this.passwordEncoder = passwordEncoder;
// NEW LINE ADDED: Stores the PasswordEncoder that Spring provided.
}
public User registerUser(User user) {
String encodedPassword =
passwordEncoder.encode(user.getPassword());
// NEW LINES ADDED:
// Takes the user's plain-text password
// and converts it into a BCrypt hashed password.
//
// Example:
// "123456"
// ↓
// passwordEncoder.encode(...)
// ↓
// "$2a$10$...."
user.setPassword(encodedPassword);
// NEW LINE ADDED:
// Replaces the plain-text password inside the User object
// with the encoded/hashed password.
return userRepository.save(user);
}
}
Now rebuild our application
mvn spring-boot:run
Now if you check output in postman.
Now if you check our database

This means our user password is now in hash format, but we dont want our password to leave backend as you
can see from POSTMAN, our hashpassword is given as response for user input.
To avoid that, we will use DTO.
Data Transfer Object
backend/src/main/java/com/maaystore/backend/dto/UserResponse.java
Notice that there is no password in this file.package com.maayastore.backend.dto; public class UserResponse { private Long id; private String name; private String email; private String role; public UserResponse() { } public UserResponse(Long id, String name, String email, String role) { this.id = id; this.name = name; this.email = email; this.role = role; } public Long getId() { return id; } public void setId(Long id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } public String getEmail() { return email; } public void setEmail(String email) { this.email = email; } public String getRole() { return role; } public void setRole(String role) { this.role = role; } }
What are we doing here?, whenever user is registered. we are focusing to save it in database and not to expose
password as response back. response can be username, email or password.
But we will be avoiding password as return response.
Now let us update UserService.Java to include our DTO
package com.maayastore.backend.service; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; import com.maayastore.backend.dto.UserResponse; import com.maayastore.backend.entity.User; import com.maayastore.backend.repository.UserRepository; @Service public class UserService { private final UserRepository userRepository; private final PasswordEncoder passwordEncoder; public UserService( UserRepository userRepository, PasswordEncoder passwordEncoder) { this.userRepository = userRepository; this.passwordEncoder = passwordEncoder; } public UserResponse registerUser(User user) { // CHANGED: Previously this returned User. // Now it returns UserResponse so that sensitive information, // such as the password hash, is not sent back to the frontend. String encodedPassword = passwordEncoder.encode(user.getPassword()); // NEW: Hashes the user's password using PasswordEncoder. // // Example: // User enters: "123456" // // Before: // user.getPassword() → "123456" // // After: // encodedPassword → "$2a$10$........" // // The actual password is NOT stored directly in the database. user.setPassword(encodedPassword); User savedUser = userRepository.save(user); // CHANGED: Previously we directly returned: // return userRepository.save(user); // // Now we save the user and store the result in savedUser. // We need savedUser to get the user's details and create // a UserResponse object. return new UserResponse( savedUser.getId(), savedUser.getName(), savedUser.getEmail(), savedUser.getRole() ); // NEW: Creates and returns a UserResponse DTO. // // Only safe information is returned: // - id // - name // - email // - role // // Notice that password is NOT included. // Therefore, the password hash is not exposed to the frontend. } }Now update AuthController.Java
package com.maayastore.backend.controller; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import com.maayastore.backend.dto.UserResponse; import com.maayastore.backend.entity.User; import com.maayastore.backend.service.UserService; @RestController @RequestMapping("/api/auth") public class AuthController { private final UserService userService; public AuthController(UserService userService) { this.userService = userService; } @PostMapping("/register") public UserResponse registerUser(@RequestBody User user) { // CHANGED: Previously this method returned User: // // public User registerUser(@RequestBody User user) // // Now it returns UserResponse. // // User contains the password field. // UserResponse contains only the information we want // to expose to the frontend. return userService.registerUser(user); // No change to this line. // // UserService now returns a UserResponse, // so the controller also returns that UserResponse. } }Now let us rebuild application and test if we still get password in responsemvn spring-boot:runand then test postmanHandling Duplication
As we login to any website, we will be able to login only once with one mail id and this is where we will alsolearn on how to avoid duplicate email idsUpdate UserService.java with following
public boolean emailExists(String email) { return userRepository.findByEmail(email).isPresent(); }
src/main/java/com/maaystore/backend/dto/ErrorResponse.java
package com.maayastore.backend.dto; public class ErrorResponse { private String message; public ErrorResponse(String message) { this.message = message; } public String getMessage() { return message; } public void setMessage(String message) { this.message = message; } }And since we want user to throw anbut now we need to check whenever user enters the same mail id, it has to check in the existing databaseand if the mail id is already present throw an errorand if the mail id is not present, continue on creating new user.so we will update our POST API for register in AuthController.javapackage com.maayastore.backend.controller; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import com.maayastore.backend.dto.ErrorResponse; import com.maayastore.backend.entity.User; import com.maayastore.backend.service.UserService; @RestController @RequestMapping("/api/auth") public class AuthController { private final UserService userService; public AuthController(UserService userService) { this.userService = userService; } @PostMapping("/register") public ResponseEntity<?> registerUser(@RequestBody User user) { if (userService.emailExists(user.getEmail())) { // NEW: Checks whether the email already exists in the database. // // user.getEmail() gets the email entered by the user. // // emailExists() calls UserRepository.findByEmail() // to check the database. return ResponseEntity .badRequest() .body(new ErrorResponse("Email already registered")); // NEW: If the email already exists: // // badRequest() → HTTP 400 status // // body(...) → sends the ErrorResponse to the frontend. // // The frontend receives something like: // // { // "message": "Email already registered" // } } return ResponseEntity.ok( userService.registerUser(user) ); } }Rebuild the application and now let us verifyAdding Validation:
Now let us add validation rules as we want user to follow certain rules like name must not be empty,email must contain @ for it to validate data, password must contain at least 6 chars.let us add dependency pom.xml<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-validation</artifactId> </dependency>also update Entity/User.java
command:package com.maayastore.backend.entity; import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; import jakarta.persistence.Table; import jakarta.validation.constraints.Email; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.Size; // NEW: Used to validate the minimum/maximum length of a field. @Entity @Table(name = "users") public class User { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @NotBlank(message = "Name is required") // NEW: Ensures that the name is provided. // If the name is empty/missing, validation returns: // "Name is required" private String name; @NotBlank(message = "Email is required") // NEW: Ensures that the email is provided. // An empty email will produce: // "Email is required" @Email(message = "Invalid email format") // NEW: Ensures that the email has a valid email format. // Example: // sai@gmail.com → valid // sai@gmail → invalid @Column(unique = true, nullable = false) private String email; @NotBlank(message = "Password is required") // NEW: Ensures that the password is provided. // If password is missing or blank: // "Password is required" @Size(min = 6, message = "Password must be at least 6 characters") // NEW: Ensures that the password has at least 6 characters. // Example: // "123" → invalid // "123456" → valid @Column(nullable = false) private String password; private String role; public User() { } public User(String name, String email, String password, String role) { this.name = name; this.email = email; this.password = password; this.role = role; } public Long getId() { return id; } public void setId(Long id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } public String getEmail() { return email; } public void setEmail(String email) { this.email = email; } public String getPassword() { return password; } public void setPassword(String password) { this.password = password; } public String getRole() { return role; } public void setRole(String role) { this.role = role; } }Now update even AuthController.java
package com.maayastore.backend.controller; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import com.maayastore.backend.dto.ErrorResponse; import com.maayastore.backend.entity.User; import com.maayastore.backend.service.UserService; import jakarta.validation.Valid; // NEW: Enables validation of the incoming User object @RestController @RequestMapping("/api/auth") public class AuthController { private final UserService userService; public AuthController(UserService userService) { this.userService = userService; } @PostMapping("/register") public ResponseEntity<?> registerUser( @Valid @RequestBody User user) { // NEW: @Valid triggers the validation rules defined in the User entity // such as @NotBlank, @Email, and @Size. // Check whether the email is already registered if (userService.emailExists(user.getEmail())) { return ResponseEntity .badRequest() .body(new ErrorResponse("Email already registered")); } return ResponseEntity.ok( userService.registerUser(user) ); } }Now rebuild application and test using postmanthough postman may not show the messages as of now, you can check application logsThis shows our validation is working perfectly fine.Let us create a GlobalExceptionHandler rule.
Why are we using GlobalException, because we can use same set of rules when we design login page
as well , like user cannot give empty mail id or password to login.backend/src/main/java/com/maayastore/backend/exception/GlobalExceptionHandler.java
Rebuild application and verify postman logs, you will be able to see the messages nowpackage com.maayastore.backend.exception; import java.util.HashMap; import java.util.Map; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; @RestControllerAdvice public class GlobalExceptionHandler { @ExceptionHandler(MethodArgumentNotValidException.class) public ResponseEntity<Map<String, String>> handleValidationErrors( MethodArgumentNotValidException exception) { Map<String, String> errors = new HashMap<>(); exception.getBindingResult() .getFieldErrors() .forEach(error -> errors.put( error.getField(), error.getDefaultMessage() ) ); return ResponseEntity.badRequest().body(errors); } }
Setting User´s Default role.
a simple change can avoid this. add user.setRole("USER"); in AuthController.java
package com.maayastore.backend.controller;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.maayastore.backend.dto.ErrorResponse;
import com.maayastore.backend.entity.User;
import com.maayastore.backend.service.UserService;
import jakarta.validation.Valid;
@RestController
@RequestMapping("/api/auth")
public class AuthController {
private final UserService userService;
public AuthController(UserService userService) {
this.userService = userService;
}
@PostMapping("/register")
public ResponseEntity<?> registerUser(
@Valid @RequestBody User user) {
if (userService.emailExists(user.getEmail())) {
return ResponseEntity
.badRequest()
.body(new ErrorResponse("Email already registered"));
}
user.setRole("USER");
// Security: Always assign the default USER role on registration.
// This prevents clients from submitting "ADMIN" as their role.
return ResponseEntity.ok(
userService.registerUser(user)
);
}
}
Now rebuild our application and test using POSTMAN
Building Registration Page.
frontend/src/pages/Register.jsx
import { useState } from "react"; function Register() { // Form fields const [name, setName] = useState(""); const [email, setEmail] = useState(""); const [password, setPassword] = useState(""); // Registration status const [loading, setLoading] = useState(false); const [success, setSuccess] = useState(""); const [error, setError] = useState(""); // Stores validation errors returned by the backend // Example: // { // name: "Name is required", // email: "Invalid email format", // password: "Password must be at least 6 characters" // } const [validationErrors, setValidationErrors] = useState({}); const handleRegister = async (event) => { event.preventDefault(); // Clear previous messages/errors before a new submission setLoading(true); setSuccess(""); setError(""); setValidationErrors({}); // Create the user object that will be sent to the backend const user = { name: name, email: email, password: password }; try { // Send registration request to Spring Boot backend const response = await fetch( "http://localhost:8080/api/auth/register", { method: "POST", headers: { "Content-Type": "application/json" }, // Convert JavaScript object into JSON body: JSON.stringify(user) } ); // Convert backend JSON response into JavaScript object const data = await response.json(); console.log("Response:", data); // Check whether backend returned an error if (!response.ok) { // Handles errors such as: // { "message": "Email already registered" } if (data.message) { setError(data.message); } // Handles validation errors such as: // { // "name": "Name is required", // "email": "Invalid email format" // } else { setValidationErrors(data); } return; } // Registration was successful setSuccess("Registration successful!"); // Clear the form after successful registration setName(""); setEmail(""); setPassword(""); } catch (error) { // Handles network/server connection errors console.error("Registration failed:", error); setError("Unable to connect to the server."); } finally { // Stop loading after the request finishes setLoading(false); } }; return ( <div className="register-page"> <div className="register-container"> <h2>Create Account</h2> <form onSubmit={handleRegister}> {/* Name */} <div className="form-group"> <label>Name</label> <input type="text" value={name} onChange={(event) => setName(event.target.value)} placeholder="Enter your name" /> {/* Display backend validation error for name */} {validationErrors.name && ( <p className="field-error"> {validationErrors.name} </p> )} </div> {/* Email */} <div className="form-group"> <label>Email</label> <input type="email" value={email} onChange={(event) => setEmail(event.target.value)} placeholder="Enter your email" /> {/* Display backend validation error for email */} {validationErrors.email && ( <p className="field-error"> {validationErrors.email} </p> )} </div> {/* Password */} <div className="form-group"> <label>Password</label> <input type="password" value={password} onChange={(event) => setPassword(event.target.value)} placeholder="Enter your password" /> {/* Display backend validation error for password */} {validationErrors.password && ( <p className="field-error"> {validationErrors.password} </p> )} </div> {/* General error message */} {error && ( <p className="error"> {error} </p> )} {/* Success message */} {success && ( <p className="success"> {success} </p> )} <button type="submit" disabled={loading}> {loading ? "Registering..." : "Register"} </button> </form> </div> </div> ); } export default Register;Now update App.jsx
import { useEffect, useState } from "react"; import { Routes, Route } from "react-router-dom"; import ProductCard from "./components/ProductCard"; import ProductDetails from "./pages/ProductDetails"; import Cart from "./pages/Cart"; import Navbar from "./components/Navbar"; import Register from "./pages/Register"; // NEW: Imports the registration page function Home() { const [products, setProducts] = useState([]); useEffect(() => { fetch("http://localhost:8080/api/products") .then(response => response.json()) .then(data => { setProducts(data); }) .catch(error => { console.error("Error fetching products:", error); }); }, []); return ( <main className="main-content"> <h2>Our Products</h2> <div className="product-grid"> {products.map(product => ( <ProductCard key={product.id} product={product} /> ))} </div> </main> ); } function App() { return ( <div className="app"> <Navbar /> <Routes> <Route path="/" element={<Home />} /> <Route path="/products/:id" element={<ProductDetails />} /> <Route path="/cart" element={<Cart />} /> <Route path="/register" element={<Register />} /> {/* NEW: Adds the /register URL and displays the Register page */} </Routes> </div> ); } export default App;Also update index.css for styling our registration page
.register-page { min-height: calc(100vh - 70px); display: flex; justify-content: center; align-items: center; background: #f7f8fa; padding: 40px 20px; } .register-container { width: 100%; max-width: 430px; background: white; padding: 40px; border-radius: 14px; box-shadow: 0 8px 30px rgba(0, 0, 0, 0.08); } .register-container h2 { margin: 0 0 30px; text-align: center; font-size: 28px; color: #04243D; } .form-group { margin-bottom: 20px; } .form-group label { display: block; margin-bottom: 8px; font-size: 14px; font-weight: 600; color: #333; } .form-group input { width: 100%; box-sizing: border-box; padding: 13px 14px; border: 1px solid #d9dde3; border-radius: 8px; font-size: 15px; outline: none; transition: border-color 0.2s ease, box-shadow 0.2s ease; } .form-group input:focus { border-color: #5354FF; box-shadow: 0 0 0 3px rgba(83, 84, 255, 0.12); } .form-group input::placeholder { color: #999; } .register-container button { width: 100%; padding: 14px; margin-top: 8px; border: none; border-radius: 8px; background: #04243D; color: white; font-size: 16px; font-weight: 600; cursor: pointer; transition: background 0.2s ease, transform 0.1s ease; } .register-container button:hover { background: #053452; } .register-container button:active { transform: scale(0.99); } .register-container button:disabled { background: #9da5ad; cursor: not-allowed; transform: none; } .success-message { padding: 12px 14px; margin-bottom: 20px; border-radius: 8px; background: #edf8f0; color: #2e7d32; font-size: 14px; } .error-message { padding: 12px 14px; margin-bottom: 20px; border-radius: 8px; background: #fff0f0; color: #c62828; font-size: 14px; } .field-error { margin: 6px 0 0; color: #d32f2f; font-size: 13px; } @media (max-width: 500px) { .register-page { padding: 25px 15px; } .register-container { padding: 30px 22px; } .register-container h2 { font-size: 24px; } }
Now let us rebuild our frontend application
command:
npm run dev



















Comments
Post a Comment