Topic 016: Nested-Categories-Management in MERN
Implement simple CRUD API to create product(name, price) & assign multiple categories to it
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)
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).
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
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));
---