💻 DevOps Maaya - Workshop 2 - September 6, 2026 | Cart, Product Details and Registration Page


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


command:

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


command:

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

command:

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


command:

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



command:

* {

  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


command:

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



command:
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>
);



npm run dev to run frontend and your website will look similar to this and mvn spring-boot:run






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

command:

    @GetMapping("/{id}")

    public Product getProductById(@PathVariable Long id) {

        return productService.getProductById(id);

    }



And now add the following lines in ProductService.Java


command:

    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



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;

@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;
    }
}


Now re-build our backend application

command:
mvn spring-boot:run

check our database if user table is created or not

command:

psql -d maaya_store_database

\dt



create UserRepository.java



command:

src/main/java/com/maayastore/backend/repository/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);

}




Create

src/main/java/com/maayastore/backend/service/UserService.java



command:

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);
    }
}





Now Auth Controller

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


POST http://localhost:8080/api/auth/register

command:
{
  "name": "Sai",
  "email": "sai@example.com",
  "password": "password123",
  "role": "USER"
}





As you could see from postman, password is getting exposed and we don´t want password to be stored in our database

as anyone who gets access to our database will be able to use passwords of users directly




Encrypting password


To Encrypt our password add the following dependency in pom.xml

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

command:
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




command:
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; } }

Notice that there is no password in this file.

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


command:

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



command:

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 response

command:
mvn spring-boot:run

and then test postman


This shows that our backend is now not sending password back to frontend application.


Handling 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 also
learn on how to avoid duplicate email ids

Update UserService.java with following



command:
public boolean emailExists(String email) {

    return userRepository.findByEmail(email).isPresent();
}




the above lines just finds if mail id exists or not.


for this we will set an error response messages using dto


src/main/java/com/maaystore/backend/dto/ErrorResponse.java



command:

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 an



but now we need to check whenever user enters the same mail id, it has to check in the existing database
and if the mail id is already present throw an error

and if the mail id is not present, continue on creating new user.

so we will update our POST API for register in AuthController.java


command:

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; @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 verify







Adding 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

command:
<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


command:

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 postman





though postman may not show the messages as of now, you can check application logs



This 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



command:
package 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); } }

Rebuild application and verify postman logs, you will be able to see the messages now





Setting User´s Default role.


Also when user wants to register, notice that we explicitly are mentioning his role as user . when we create seperate pages for user and admin. we dont want user to have privileges to have admin user.

a simple change can avoid this. add  user.setRole("USER"); in AuthController.java


command:
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.

Now let us use UI to build a proper registration page as using backend we have built all the logics required.

frontend/src/pages/Register.jsx




command:

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



command:

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



command:
.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





We have now successfully created our Registration page, cart page, product Details page and Validation of Users in this Workshop.

In Next Workshop, we will discuss on how to use Login page, authenticate into website with password and then stay logged in using tokens which are generated by backend.



Comments