DEV Community

krishnateja
krishnateja

Posted on

Building a Product Store with MERN Stack

Technologies Used

**Frontend:

  • React.js: Used to build the user interface and handle dynamic page rendering.
  • Chakra UI: A component library that provides pre-built, accessible UI components, allowing for easy styling and responsive design.
  • React Router: For navigating between pages (e.g., Home page, Create page).
  • React Hooks: Specifically, useState, useEffect, and useContext are used to manage state, side effects, and application data flow.
  • Backend:
  • Node.js: Server-side runtime for handling API requests.
  • Express.js: Web framework for Node.js to build APIs for product management (CRUD operations).
  • MongoDB: NoSQL database used for storing product information (name, price, image, description).
  • Additional Libraries:
  • Chakra UI: For styling and UI components.
  • React Icons: For adding icons like edit and delete.
  • React Toastify: For showing toast notifications to alert the user after performing actions.
  • Axios: To make HTTP requests for interacting with the backend API.

BACKEND SETUP

INTRODUCTION
Purpose of the Project: Introduce the project as a product store API that allows for managing a collection of products, including features like product listing, creating, updating, and deleting items.
Tech Stack: Mention that the backend is built with Express, Node.js, and MongoDB, with the use of dotenv for environment configuration, cors for cross-origin requests, and mongoose for handling database operations.

Dependencies and Project Structure
Dependencies: Briefly explain why each dependency is essential.

  1. express: For setting up server routes and handling requests.
  2. dotenv: For managing environment variables like database URIs.
  3. mongoose: For connecting to MongoDB and creating a data model.
  4. cors: To handle cross-origin requests, allowing the frontend to access backend APIs.
  5. cross-env: To set environment variables consistently across different operating systems.
  6. nodemon: For hot-reloading in development, making it easier to test changes.
  • Folder Structure: Highlight key folders and files like server.js for server logic, product.model.js for the MongoDB schema, dbConfig.js for the database connection, and product.route.js for routing requests.

Database Configuration``

dbConfig.js
Describe the connectDB function:
This function uses mongoose.connect() to connect to MongoDB, utilizing the MONGODB_URI from an .env file.
Error handling ensures the server exits if the database connection fails, maintaining server stability.
Environment Configuration: Mention the importance of using .env files to securely manage sensitive information like database URIs.

`
import mongoose from "mongoose";

export const connectDB = async () => {
try {
const conn = await mongoose.connect(process.env.MONGODB_URI);
console.log(Connected to MongoDB :${conn.connection.host});
} catch (error) {
console.error(error : ${error.message});
process.exit(1);
}
};
`

Setting Up the Product Schema

Product Model (product.model.js)
Describe the Product schema using Mongoose:
Fields: Includes fields like name, price, image, and description, each with required validation.
Timestamps: The schema uses timestamps, which automatically adds createdAt and updatedAt fields, useful for tracking data changes.

`import mongoose from "mongoose";

const productSchema = new mongoose.Schema({
name: { type: String, required: true },
price: { type: Number, required: true },
image: { type: String, required: true },
description: { type: String, required: true },
}, {
timestamps: true,
});

const Product = mongoose.model("Product", productSchema);
export default Product;
`

Server Configuration and API Endpoints

Server Setup (index.js)
Explain how the server is initialized:
Express App: Creates an instance of Express, with middleware for parsing JSON and handling CORS.
Basic Route: A root route (/) to verify server health, which responds with a welcome message.
Product Routes: Attaches product-related routes (/api/products), allowing CRUD operations for product data.
Static File Serving: (For production) Configured to serve static files, ensuring smooth integration with frontend build files.

`import express from "express";
import dotenv from "dotenv";
import { connectDB } from "./config/db.js";
import router from "./routes/product.route.js";
import cors from "cors";

dotenv.config();
const app = express();
app.use(express.json());
app.use(cors());

app.get("/", (req, res) => {
res.send("hello krishna how are you");
});

app.use("/api/products", router);

app.listen(3000, () => {
connectDB();
console.log("Server is running on http://localhost:3000");
});
`

Here’s a blog post section highlighting the API routes and controllers for handling CRUD operations in your Product Store backend:

API Routes and Controllers for Product Management

In this section, I'll walk through the core API functionality, explaining how each route and controller works to create, read, update, and delete products.

  1. Retrieving All Products The getProducts function retrieves all products stored in the database and returns them in JSON format.

Code Explanation:

  1. Function: getProducts Route: GET /api/products Logic: It uses Mongoose’s .find() method to fetch all products, sending a 200 response with product data or a 500 error if something goes wrong.

export const getProducts = async (req, res) => {
try {
const products = await Product.find();
res.status(200).json({ success: true, data: products });
console.log(products);
} catch (err) {
console.error(err);
res.status(500).json({ message: "Error fetching products" });
}
};

  1. Adding a New Product The postProducts function allows users to add a new product by sending product details in the request body.

Code Explanation:

Function: postProducts
Route: POST /api/products
Logic: First, it validates that all fields are provided. If any field is missing, it sends a 400 status error. If all fields are present, it creates a new product instance and saves it to the database. Success and error messages are handled accordingly.
`export const postProducts = async (req, res) => {
const product = req.body;
if (!product.name || !product.price || !product.image || !product.description) {
return res.status(400).json({ success: false, message: "Please fill all the fields" });
}

const newProduct = new Product(product);
try {
await newProduct.save();
res.status(201).json({
success: true,
message: "Product created successfully",
data: newProduct,
});
console.log(newProduct);
} catch (error) {
console.log("error in create product", error.message);
res.status(500).json({
success: false,
message: "Error creating product",
});
}
};
`

  1. Updating an Existing Product The updateProduct function updates product details based on a unique product ID provided in the request.

Code Explanation:

Function: updateProduct
Route: PUT /api/products/:id
Logic: The function first checks if the product ID is valid using Mongoose’s Types.ObjectId.isValid() method. If valid, it proceeds with the update using findByIdAndUpdate(), returning the updated product or an error if the update fails.
`export const updateProduct = async (req, res) => {
const { id } = req.params;
const productData = req.body;

if (!mongoose.Types.ObjectId.isValid(id)) {
return res.status(404).json({ success: false, message: "Invalid product id" });
}

try {
const updatedProduct = await Product.findByIdAndUpdate(id, productData, { new: true });
res.status(200).json({
success: true,
data: updatedProduct,
message: "Product updated successfully",
});
} catch (error) {
console.log("error in update product", error.message);
res.status(500).json({
success: false,
message: "Error updating product",
});
}
};
`

  1. Deleting a Product The deleteProduct function deletes a product based on the provided product ID.

Code Explanation:

Function: deleteProduct
Route: DELETE /api/products/:id
Logic: It first validates the product ID. If valid, it deletes the product using findByIdAndDelete() and sends a success message; otherwise, it handles errors with appropriate status codes and messages.
javascript
Copy code
`export const deleteProduct = async (req, res) => {
const { id } = req.params;

if (!mongoose.Types.ObjectId.isValid(id)) {
return res.status(404).json({ success: false, message: "Invalid product id" });
}
try {
await Product.findByIdAndDelete(id);
console.log(product deleted with this id : ${id});
res.status(200).json({
success: true,
message: "product deleted successfully",
});
} catch (error) {
console.log("error in delete product", error.message);
res.status(500).json({
success: false,
message: "Error deleting product",
});
}
};
`

Backend (Express.js and MongoDB)

Express.js handles the backend routes:

GET /products: Fetches all products from the MongoDB database.
POST /products: Adds a new product to the database.
PUT /products/
: Updates an existing product with the specified id.
DELETE /products/
: Deletes a product with the specified id.
MongoDB stores the products, each with fields like:

name: The product name.
price: The product price.
image: URL of the product image.
description: A brief description of the product.

**Frontend Setup and Initial Rendering with React

**To build an interactive and responsive user interface, we’re using React along with Chakra UI for styling and React Router for seamless navigation.

Code Overview
In the main file, we initialize and render the entire app by wrapping it in several essential providers:

  • StrictMode: Enables additional checks and warnings in development mode.
  • BrowserRouter: Allows us to use React Router for handling client-side routing.
  • ChakraProvider: Provides a consistent, responsive UI design with Chakra UI components.
  • javascript
  • Copy code

`import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import { ChakraProvider } from "@chakra-ui/react";
import { BrowserRouter } from "react-router-dom";

import App from "./App.jsx";

createRoot(document.getElementById("root")).render(







);
`

App Component Structure

In App.jsx, we define the primary structure and layout for the Product Store frontend. Here, we use React Router to handle navigation and Chakra UI’s Box component to wrap our layout with a flexible, responsive design.

Code Explanation
Routes and Components:

Homepage and CreatePage components are set up to handle the main functionalities of viewing and creating products, respectively.
Route paths: The root path (/) loads Homepage, while /create loads the CreatePage component.
Chakra UI Components:

Box: Used as a wrapper, it covers the full height of the viewport (minH={"100vh"}) and applies a background color based on the color mode (light or dark) using useColorModeValue.
Navbar: A navigation component that appears across all pages for consistent user experience.
javascript
Copy code

`import { Box } from "@chakra-ui/react";
import { Routes, Route } from "react-router-dom";
import Homepage from "./pages/Homepage";
import CreatePage from "./pages/CreatePage";
import React from "react";
import Navbar from "./components/Navbar";
import { useColorModeValue } from "@chakra-ui/react";

const App = () => {
return (



} />
} />


);
};

export default App;
`

State Management with Zustand and API Requests with Axios

For efficient state management and handling API requests, we’ve utilized Zustand and Axios. Zustand is a simple, fast, and scalable state management library that allows us to keep the application’s state centralized and easily manageable. Axios is used for making HTTP requests to the backend API to perform CRUD operations on the products.

State Management with Zustand
In this project, we create a custom store using Zustand to manage the products data. Zustand helps in storing the products and updating them without the need for complex Redux-like setups.
`import { create } from "zustand";
import axios from "axios";

export const useProductStore = create((set) => ({
products: [],
setProducts: (products) => set({ products }),

createProduct: async (newProduct) => {
if (!newProduct.name || !newProduct.image || !newProduct.price) {
return {
success: false,
message: "please fill all the fields.",
};
}
const data = await axios.post(
"http://localhost:3000/api/products",
newProduct
);
set((state) => ({ products: [...state.products, data.data] }));
return { success: true, message: "Product created successfully." };
},

fetchProducts: async () => {
const data = await axios.get("http://localhost:3000/api/products");
set({ products: data.data.data });
},
deleteProduct: async (pid) => {
try {
const { data } = await axios.delete(
http://localhost:3000/api/products/${pid}
);
set((state) => ({
products: state.products.filter((product) => product._id !== pid),
}));
return { success: true, message: data.message };
} catch (error) {
return {
success: false,
message: error.response?.data?.message || "Failed to delete product",
};
}
},

updateProduct: async (pid, updatedProduct) => {
try {
const response = await axios.put(
http://localhost:3000/api/products/${pid},
updatedProduct
);
const data = response.data;

  if (!data.success) {
    return { success: false, message: data.message || "Update failed." };
  }

  set((state) => ({
    products: state.products.map((product) =>
      product._id === pid ? data.data : product
    ),
  }));
  return { success: true, message: data.message || "Product updated successfully." };
} catch (error) {
  return {
    success: false,
    message: error.response?.data?.message || "Failed to update product",
  };
}
Enter fullscreen mode Exit fullscreen mode

}
}));
`

Displaying Products on the Homepage

The Homepage component is responsible for rendering the list of products retrieved from the backend and displaying them in an attractive grid layout. Chakra UI's SimpleGrid component is used to manage the responsive grid of product cards, making it easy to adjust the layout for different screen sizes.

Fetching and Displaying Products
On page load, the component fetches products using the fetchProducts function from the Zustand store, which asynchronously retrieves the products from the backend.

`import { Container, VStack, Text, SimpleGrid } from "@chakra-ui/react";
import { Link } from "react-router-dom";
import React, { useEffect } from "react";
import { useProductStore } from "../store/product";
import ProductCard from "../components/ProductCard";

const Homepage = () => {
const { fetchProducts, products } = useProductStore();

useEffect(() => {
fetchProducts();
console.log(products);
}, [fetchProducts]);
return (


fontSize={"30"}
fontWeight={"bold"}
textTransform={"uppercase"}
bgClip={"text"}
bgGradient={"linear(to-r,cyan.400,blue.500)"}
>
Current Products 🛍️

    <SimpleGrid
      columns={{
        base: 1,
        md: 2,
        lg: 3,
      }}
      spacing={10}
      w={"full"}
    >
      { products.map((product) => (
        <ProductCard key={product._id} product={product} />
      ))}
    </SimpleGrid>

 {
  products.length === 0 && (
    <Text
    fontSize="xl"
    textAlign={"center"}
    fontWeight={"bold"}
    color={"gray.500"}
  >
    No products found 🥲
    <Link to={"/create"}>
      <Text
        as={"span"}
        color={"blue.500"}
        _hover={{ textDecoration: "underline" }}
      >
        Create a new product
      </Text>
    </Link>
  </Text>
  )

 }
  </VStack>
</Container>
Enter fullscreen mode Exit fullscreen mode

);
};

export default Homepage;
`

Navbar Component Breakdown

Main Features:
Branding: The store’s name is displayed using a gradient text, and clicking on it navigates to the homepage.
Product Creation Link: A button with a PlusSquareIcon is used to navigate to the product creation page (/create).
Color Mode Toggle: A button toggles between light and dark themes using Chakra UI's useColorMode hook.
Product Count: Although not visible in the current code, you have access to the list of products (useProductStore), which can be used to display product counts or other related information.
Chakra UI Styling:
Flexbox Layout: The layout of the navbar is designed using Chakra UI’s Flex component. It adjusts for different screen sizes (base and sm breakpoints), stacking the elements in a column on smaller screens (mobile-first design).
Responsive Design: The text size changes based on the screen size, and the button layout changes from row (for larger screens) to column (for smaller screens).

`import React from "react";
import {
Container,
Flex,
Text,
HStack,
Button,
useColorMode,
useColorModeValue,
} from "@chakra-ui/react";
// import { FaPlusSquare } from "react-icons/fa";
import { Link } from "react-router-dom";
import { PlusSquareIcon } from "@chakra-ui/icons";
import {IoMoon} from "react-icons/io5"
import {LuSun} from "react-icons/lu"
import { useProductStore } from "../store/product";

const Navbar = () => {
const { colorMode, toggleColorMode } = useColorMode();
const {products}=useProductStore()
return (

h={16}
alignItems={"center"}
justifyContent={"space-between"}
flexDirection={{
base: "column",
sm: "row",
}}
>
fontSize={{
base: "22",
sm: "28",
}}
fontWeight={"bold"}
textTransform={"uppercase"}
bgClip={"text"}
bgGradient={"linear(to-r,cyan.400,blue.500)"}
>
Common Store 🛒

    <HStack spacing={2} alignItems={"center"}>
      <Link to={"/create"}>
        <Button>
          <PlusSquareIcon fontSize={20} />
        </Button>
      </Link>

      <Button onClick={toggleColorMode}>
        {colorMode === "light" ? <IoMoon/> : <LuSun/>}
      </Button>
    </HStack>
  </Flex>
</Container>
Enter fullscreen mode Exit fullscreen mode

);
};

export default Navbar;
`

Creating a New Product

The CreatePage component is responsible for allowing users to add new products to the store. It contains a form with input fields for the product's name, price, image, and description. When the form is submitted, it sends the new product data to the backend via the createProduct function, which is managed by Zustand. Here's a breakdown of how it works:

State Management for New Product
The component uses React's useState hook to manage the input fields for the new product. The newProduct object holds the values for name, price, image, and description, which are updated as the user types in the input fields.

const [newProduct, setNewProduct] = useState({
name: "",
price: "",
image: "",
description: "",
});

  1. newProduct: The state object storing the new product data.
  2. setNewProduct: The function used to update the state when the user types in the form fields.

Handling Product Creation

When the user clicks the "Add product" button, the handleAddProduct function is called. This function sends the newProduct data to the createProduct function from Zustand, which handles the API request to add the new product.

const handleAddProduct = async () => {
const { success, message } = await createProduct(newProduct);
if (success) {
toast({
title: "Product created",
description: message,
status: "success",
duration: 2000,
});
} else {
toast({
title: "Error",
description: message,
status: "error",
duration: 2000,
});
}
};

  • createProduct: This function from the Zustand store makes a POST request to the backend API to add the product.
  • toast: A Chakra UI Toast notification is displayed based on whether the product creation was successful or not.

Resetting Form After Submission

After the product is created successfully or if there is an error, the form fields are reset by updating the newProduct state to its initial empty values.
setNewProduct({
name: "",
price: "",
image: "",
description: "",
});

Form UI

The form uses Chakra UI components for a clean and responsive layout. The input fields are styled with Chakra UI's Input component, and the form buttons are styled with the Button component.

<VStack spacing={4}>
<Input
placeholder="Product Name"
name="name"
value={newProduct.name}
onChange={(e) =>
setNewProduct({ ...newProduct, name: e.target.value })
}
/>
<Input
placeholder="Product Price"
name="price"
value={newProduct.price}
onChange={(e) =>
setNewProduct({ ...newProduct, price: e.target.value })
}
/>
<Input
placeholder="Product Image"
name="image"
value={newProduct.image}
onChange={(e) =>
setNewProduct({ ...newProduct, image: e.target.value })
}
/>
<Input
placeholder="Product Description"
name="description"
value={newProduct.description}
onChange={(e) =>
setNewProduct({ ...newProduct, description: e.target.value })
}
/>
<Button colorScheme={"blue"} onClick={handleAddProduct}>
Add product
</Button>
</VStack>

  • Input: Each input field is controlled using React’s value prop, which is bound to the corresponding property in the newProduct state.
  • Button: The button triggers the handleAddProduct function, which handles the product creation process.

Navigation and UI Design

The Back to Home button uses React Router's Link component to navigate the user back to the homepage after they create a product.
Chakra UI's VStack and Box components are used for vertical stacking and layout, and useColorModeValue is used to manage light/dark theme styling.

<Link to={"/"}>
<Button colorScheme={"gray"} mt={4}>
Back to Home
</Button>
</Link>

ProductCard Component Breakdown

Main Features:
View Product Details: Displays product information like name, price, and image, providing a clear view of the product.
Edit Product: Opens a modal where users can update the product's name, price, image, and description.
Delete Product: Allows users to delete the product with a confirmation toast message.
Responsive Design: Uses Chakra UI components for responsive design, making it mobile-friendly and adaptable to different screen sizes.
Toast Notifications: Provides feedback to the user with success or error messages after performing actions like update or delete.

`import {
Box,
Heading,
HStack,
IconButton,
Image,
Modal,
ModalBody,
ModalCloseButton,
ModalOverlay,
ModalContent,
ModalHeader,
ModalFooter,
Text,
useColorModeValue,
useDisclosure,
useToast,
VStack,
Input,
Button,
} from "@chakra-ui/react";
import { DeleteIcon, EditIcon } from "@chakra-ui/icons";
import React, { useState } from "react";
import { useProductStore } from "../store/product";

const ProductCard = ({ product }) => {
const textColor = useColorModeValue("gray.600", "gray.200");
const bg = useColorModeValue("white", "gray.800");
const { deleteProduct, updateProduct } = useProductStore();
const { isOpen, onClose, onOpen } = useDisclosure();
const toast = useToast();
const [updatedProduct, setUpdatedProduct] = useState(product);

const handleDeleteProduct = async (pid) => {
const { success, message } = await deleteProduct(pid);
toast({
title: success ? "Product deleted" : "Error deleting product",
description: message,
status: success ? "success" : "error",
duration: 2000,
isClosable: true,
});
};

const handleUpdateProduct = async (pid, updatedProduct) => {
const { success, message } = await updateProduct(pid, updatedProduct);
toast({
title: success ? "Product updated" : "Error updating product",
description: message,
status: success ? "success" : "error",
duration: 2000,
isClosable: true,
});
if (success) onClose();
};

return (
shadow="lg"
rounded="lg"
overflow="hidden"
transform="all 0.3s"
_hover={{ transform: "translateY(-5px)", shadow: "xl" }}
bg={bg}
>



{product.name}


{product.price}


} onClick={onOpen} colorScheme="blue" />
icon={}
colorScheme="red"
onClick={() => handleDeleteProduct(product._id)}
/>

  <Modal isOpen={isOpen} onClose={onClose}>
    <ModalOverlay />
    <ModalContent>
      <ModalHeader>Update Product</ModalHeader>
      <ModalCloseButton />
      <ModalBody>
        <VStack spacing={4}>
          <Input
            placeholder="Product Name"
            name="name"
            value={updatedProduct.name || ""}
            onChange={(e) =>
              setUpdatedProduct((prev) => ({ ...prev, name: e.target.value }))
            }
          />
          <Input
            placeholder="Product Price"
            name="price"
            value={updatedProduct.price || ""}
            onChange={(e) =>
              setUpdatedProduct((prev) => ({ ...prev, price: e.target.value }))
            }
          />
          <Input
            placeholder="Product Image"
            name="image"
            value={updatedProduct.image || ""}
            onChange={(e) =>
              setUpdatedProduct((prev) => ({ ...prev, image: e.target.value }))
            }
          />
          <Input
            placeholder="Product Description"
            name="description"
            value={updatedProduct.description || ""}
            onChange={(e) =>
              setUpdatedProduct((prev) => ({ ...prev, description: e.target.value }))
            }
          />
        </VStack>
      </ModalBody>
      <ModalFooter>
        <Button
          colorScheme="blue"
          mr={3}
          onClick={() => handleUpdateProduct(product._id, updatedProduct)}
        >
          Update
        </Button>
        <Button colorScheme="gray" onClick={onClose}>
          Cancel
        </Button>
      </ModalFooter>
    </ModalContent>
  </Modal>
</Box>
Enter fullscreen mode Exit fullscreen mode

);
};

export default ProductCard;
`

Flow of the Application

  • Homepage (View Products):
  • When the app loads, the Homepage fetches the list of products from the server via an API call.
  • Products are displayed in a grid, and users can interact with them via buttons to edit or delete.
  • If no products exist, a message is displayed with a link to create a new product.
  • Create New Product:
  • Users can navigate to the CreatePage, where they fill in a form with product details (name, price, image, description).
  • After submitting the form, the app makes a POST request to the backend to save the new product in the database.
  • On success, a success message appears via a toast notification.
  • Edit and Delete Product:
  • Each product displayed on the homepage has options to edit or delete.
  • When the user clicks edit, a modal opens pre-filled with the current product details. The user can update the fields and submit the changes.
  • When the user clicks delete, a delete request is made to the backend, and the product is removed from the database. A toast notification confirms the action.

Top comments (0)