nodejsnotes

Topic 016: Nested-Categories-Management in MERN

Topic 016: Nested-Categories-Management in MERN

  1. Implement simple CRUD API to create product(name, price) & assign multiple categories to it

  2. Category can be subcategory of other category & possibly N number of depth. check below example :

Category 1
    sub category 1 ( N number of depth)
        sub sub category ( N number of depth)
            sub sub sub category( N number of depth)
    Sub category 2 (N number of depth)
    Sub category 3 (N number of depth)
    Sub category 3 (N number of depth)


Category N (N number of depth)
  1. Integrate API in React with List, add, edit, delete and assign categories.

To create a nested categories management system in a MERN stack, we need to implement both the backend (using Node.js, Express, and MongoDB) and the frontend (using React).

Backend Implementation

1. Setting up the Backend

First, initialize a Node.js project and install the necessary dependencies.

mkdir nested-categories-mern
cd nested-categories-mern
npm init -y
npm install express mongoose body-parser cors

Create the following file structure for the backend:

backend/
  ├── models/
  │   ├── Category.js
  │   └── Product.js
  ├── routes/
  │   ├── categories.js
  │   └── products.js
  ├── .env
  ├── server.js

2. Define Models

backend/models/Category.js

const mongoose = require("mongoose");

const categorySchema = new mongoose.Schema({
  name: { type: String, required: true },
  parent: { type: mongoose.Schema.Types.ObjectId, ref: "Category", default: null },
});

module.exports = mongoose.model("Category", categorySchema);

backend/models/Product.js

const mongoose = require("mongoose");

const productSchema = new mongoose.Schema({
  name: { type: String, required: true },
  price: { type: Number, required: true },
  categories: [{ type: mongoose.Schema.Types.ObjectId, ref: "Category" }],
});

module.exports = mongoose.model("Product", productSchema);

3. Define Routes

backend/routes/categories.js

const express = require("express");
const router = express.Router();
const Category = require("../models/Category");

// Create category
router.post("/", async (req, res) => {
  try {
    const category = new Category(req.body);
    await category.save();
    res.status(201).send(category);
  } catch (error) {
    res.status(400).send(error);
  }
});

// Get all categories
router.get("/", async (req, res) => {
  try {
    const categories = await Category.find().populate("parent");
    res.send(categories);
  } catch (error) {
    res.status(400).send(error);
  }
});

// Get category by ID
router.get("/:id", async (req, res) => {
  try {
    const category = await Category.findById(req.params.id).populate("parent");
    res.send(category);
  } catch (error) {
    res.status(400).send(error);
  }
});

// Update category
router.put("/:id", async (req, res) => {
  try {
    const category = await Category.findByIdAndUpdate(req.params.id, req.body, {
      new: true,
      runValidators: true,
    });
    res.send(category);
  } catch (error) {
    res.status(400).send(error);
  }
});

// Delete category
router.delete("/:id", async (req, res) => {
  try {
    const category = await Category.findByIdAndDelete(req.params.id);
    res.send(category);
  } catch (error) {
    res.status(400).send(error);
  }
});

module.exports = router;

backend/routes/products.js

const express = require("express");
const router = express.Router();
const Product = require("../models/Product");

// Create product
router.post("/", async (req, res) => {
  try {
    const product = new Product(req.body);
    await product.save();
    res.status(201).send(product);
  } catch (error) {
    res.status(400).send(error);
  }
});

// Get all products
router.get("/", async (req, res) => {
  try {
    const products = await Product.find().populate("categories");
    res.send(products);
  } catch (error) {
    res.status(400).send(error);
  }
});

// Get product by ID
router.get("/:id", async (req, res) => {
  try {
    const product = await Product.findById(req.params.id).populate("categories");
    res.send(product);
  } catch (error) {
    res.status(400).send(error);
  }
});

// Update product
router.put("/:id", async (req, res) => {
  try {
    const product = await Product.findByIdAndUpdate(req.params.id, req.body, {
      new: true,
      runValidators: true,
    });
    res.send(product);
  } catch (error) {
    res.status(400).send(error);
  }
});

// Delete product
router.delete("/:id", async (req, res) => {
  try {
    const product = await Product.findByIdAndDelete(req.params.id);
    res.send(product);
  } catch (error) {
    res.status(400).send(error);
  }
});

module.exports = router;

4. Server Setup

backend/server.js

const express = require("express");
const mongoose = require("mongoose");
const bodyParser = require("body-parser");
const cors = require("cors");
require("dotenv").config();

const app = express();
app.use(bodyParser.json());
app.use(cors());

const categoryRoutes = require("./routes/categories");
const productRoutes = require("./routes/products");

app.use("/api/categories", categoryRoutes);
app.use("/api/products", productRoutes);

const PORT = process.env.PORT || 5000;

mongoose
  .connect(process.env.MONGO_URI, { useNewUrlParser: true, useUnifiedTopology: true })
  .then(() => {
    app.listen(PORT, () => {
      console.log(`Server running on port ${PORT}`);
    });
  })
  .catch((err) => console.error(err));

5. Environment Variables

backend/.env

MONGO_URI=mongodb://localhost:27017/nested-categories

Frontend Implementation

1. Setting up the Frontend

Initialize a React project and install necessary dependencies.

npx create-react-app frontend
cd frontend
npm install axios react-router-dom

Create the following file structure for the frontend:

frontend/
  ├── src/
  │   ├── components/
  │   │   ├── CategoryForm.js
  │   │   ├── CategoryList.js
  │   │   ├── ProductForm.js
  │   │   └── ProductList.js
  │   ├── pages/
  │   │   ├── CategoriesPage.js
  │   │   └── ProductsPage.js
  │   ├── App.js
  │   ├── api.js
  │   └── index.js

2. Create API Helper

frontend/src/api.js

import axios from "axios";

const API_URL = "http://localhost:5000/api";

export const getCategories = () => axios.get(`${API_URL}/categories`);
export const createCategory = (category) => axios.post(`${API_URL}/categories`, category);
export const updateCategory = (id, category) => axios.put(`${API_URL}/categories/${id}`, category);
export const deleteCategory = (id) => axios.delete(`${API_URL}/categories/${id}`);

export const getProducts = () => axios.get(`${API_URL}/products`);
export const createProduct = (product) => axios.post(`${API_URL}/products`, product);
export const updateProduct = (id, product) => axios.put(`${API_URL}/products/${id}`, product);
export const deleteProduct = (id) => axios.delete(`${API_URL}/products/${id}`);

3. Components and Pages

frontend/src/components/CategoryForm.js

import React, { useState, useEffect } from "react";
import { getCategories, createCategory, updateCategory } from "../api";

const CategoryForm = ({ category, onSave, onCancel }) => {
  const [name, setName] = useState(category ? category.name : "");
  const [parent, setParent] = useState(category ? category.parent : null);
  const [categories, setCategories] = useState([]);

  useEffect(() => {
    getCategories().then((response) => setCategories(response.data));
  }, []);

  const handleSubmit = async (e) => {
    e.preventDefault();
    const newCategory = { name, parent };
    if (category) {
      await updateCategory(category._id, newCategory);
    } else {
      await createCategory(newCategory);
    }
    onSave();
  };

  return (
    <form onSubmit={handleSubmit}>
      <div>
        <label>Name:</label>
        <input value={name} onChange={(e) => setName(e.target.value)} required />
      </div>
      <div>
        <label>Parent Category:</label>
        <select value={parent} onChange={(e) => setParent(e.target.value)}>
          <option value="">None</option>
          {categories.map((cat) => (
            <option key={cat._id} value={cat._id}>
              {cat.name}
            </option>
          ))}
        </select>
      </div>
      <button type="submit">Save</button>
      <button type="button" onClick={onCancel}>
        Cancel
      </button>
    </form>
  );
};

export default CategoryForm;

frontend/src/components/CategoryList.js

import React, { useState, useEffect } from 'react';
import { getCategories, deleteCategory } from '../api';

const CategoryList = ({ onEdit }) => {
  const [categories, setCategories] = useState([]);

  useEffect(() => {
    loadCategories();
  }, []);

  const loadCategories = () => {
    getCategories().then(response => setCategories(response.data));

---