Comprehensive Project: E-commerce Review System (6 Modules)
Integrated projects are the best way to assess learning outcomes—this course builds a complete e-commerce review system through six modules, tying together all the concepts covered in Phases 1–5.
1. Project Overview
Project: ShopHub Review System Architecture: Node.js + Express + Mongoose + MongoDB + JWT Features: User authentication, product management, nested comments, aggregated analytics, permission control, Atlas deployment
System Architecture Design Approach: The e-commerce review system adopts a classic three-tier architecture—the routing layer (URL mapping + middleware), the business layer (Controller handling request logic), and the data layer (Mongoose Model defining data structures and validation). The separation of these three layers allows each layer to be tested and modified independently: changing API paths does not affect business logic, and changing query methods does not affect routing definitions.
Architecture Selection Decision-Making Process:
| Decision Point | Option A | Option B | Choice | Reason |
|---|---|---|---|---|
| Web Frameworks | Express | Koa / Fastify | Express | Has the most mature ecosystem and the most tutorial resources |
| ODM | Mongoose | Native Driver | Mongoose | Schema Validation + Middleware + Populate |
| Authentication Schemes | JWT | Session | JWT | Stateless, easily scalable, suitable for APIs |
| Comment Structure | Nested Documents | Citation Mode | Citation Mode (parentId) | Flexible Depth, Avoiding Nested Limits |
| Deployment Platform | Atlas + Render | Self-hosted | Atlas + Render | Managed service that saves on operations and maintenance, suitable for small and medium-sized projects |
Data Modeling Decisions: The comment system opted for a reference model (where parentId points to the parent comment) rather than nested documents—nested documents are subject to the 16MB BSON limit, and deeply nested replies are difficult to query and paginate. Although the reference model requires additional queries to assemble the tree structure, it supports unlimited levels and flexible sorting.
graph TB
Client[Browser/Mobile App] -->|HTTP| Express[Express Server]
subgraph "Express Routing Layer"
Express --> AuthMW[auth Routing<br/>Register/Log In]
Express --> ProductMW[products Routing<br/>CRUD]
Express --> ReviewMW[reviews Routing<br/>Comments/Likes]
end
subgraph "Controller Layer"
AuthMW --> AuthCtrl[authController]
ProductMW --> ProdCtrl[productController]
ReviewMW --> RevCtrl[reviewController]
end
subgraph "Model Layer"
AuthCtrl --> UserModel[User Model]
ProdCtrl --> ProdModel[Product Model]
RevCtrl --> RevModel[Review Model]
end
subgraph "MongoDB Gathering"
UserModel --> Users[(users)]
ProdModel --> Products[(products)]
RevModel --> Reviews[(reviews)]
end
style Express fill:#d4edda
style Reviews fill:#cce5ff
Data Model Relationships:
erDiagram
USER ||--o{ REVIEW : "writes"
PRODUCT ||--o{ REVIEW : "has"
REVIEW ||--o{ REVIEW : "parent reply"
USER {
ObjectId _id PK
string email UK
string username UK
string passwordHash
string role
boolean isActive
}
PRODUCT {
ObjectId _id PK
string sku UK
string title
number price
string category
number rating
number reviewCount
}
REVIEW {
ObjectId _id PK
ObjectId productId FK
ObjectId userId FK
string content
number rating
ObjectId parentId FK
number likeCount
boolean isApproved
}
2. Module 1: Project Initialization
A Detailed Explanation of the Architecture Selection Process: Choosing the right technology for an e-commerce review system isn’t about “going with whatever’s popular,” but rather finding the optimal solution based on constraints—1. Choose Express over NestJS: For small- to medium-sized projects, NestJS’s decorators and dependency injection (DI) add complexity without providing additional benefits; 2. Choose Mongoose over the native driver: Schema validation and middleware mechanisms are critical for a review system (password hashing, soft-delete filtering); 3. Choose JWT over sessions: API services require stateless operation; sessions require shared Redis storage, which increases operational costs; 4. Choose the reference model over nested documents: The number of reviews is unpredictable, and nested documents carry the risk of hitting the 16MB limit.
Principles of Module Interaction: The dependencies among the six modules form a clear hierarchy—Module 1 (Initialization) provides the infrastructure; Module 2 (Model) defines the data contract; Module 3 (CRUD) implements the business logic; Module 4 (Aggregation) adds analytical capabilities; Module 5 (Security) strengthens protection; and Module 6 (Deployment) completes the deployment. Each module depends only on the preceding module and has no backward dependencies, which allows development to proceed iteratively—first completing Modules 1–3 to obtain a usable API, then gradually adding aggregation, security, and deployment.
Design Principles for Project Initialization: Project initialization is not just about running "npm init"; it is also a process of defining the project structure, selecting dependencies, and establishing configuration management strategies. A well-structured project should reflect the MVC architecture: data definitions in models/, business logic in controllers/, route mappings in routes/, and cross-cutting concerns (such as authentication and error handling) in middlewares/.
Principles for Designing Directory Structures:
| Table of Contents | Responsibilities | Dependency Directions | Testing Strategy |
|---|---|---|---|
| models/ | Data Definition + Validation | No External Dependencies | Unit Tests |
| controllers/ | Request handling + coordination | Depends on models | Integration testing |
| routes/ | URL → Controller Mapping | Dependencies: controllers + middlewares | Route Testing |
| middlewares/ | Authentication/Authorization/Error Handling | Dependency Config | Unit Testing |
| validators/ | joi Schema Definition | No External Dependencies | Unit Tests |
| utils/ | Utility Functions | No External Dependencies | Unit Tests |
Reasons for Selecting Dependencies:
| Dependency | Purpose | Why Choose It |
|---|---|---|
| Express | Web framework | Most mature, with a rich middleware ecosystem |
| Mongoose | ODM | Schema validation + populate + middleware |
| jsonwebtoken | JWT Authentication | Stateless authentication, suitable for APIs |
| bcrypt | Password hashing | Industry standard, resistant to rainbow tables |
| joi | Input Validation | Schema-style, reusable |
| dotenv | Environment Variables | 12-Factor App Guidelines |
| cors | cross-origin | API essentials |
(1) Project Structure
shophub-reviews/
├── package.json
├── .env
├── .env.example
├── src/
│ ├── app.js # Express Applications
│ ├── config/
│ │ └── db.js # mongoose Connect
│ ├── models/ # Data Model
│ │ ├── User.js
│ │ ├── Product.js
│ │ └── Review.js
│ ├── controllers/ # Business Logic
│ │ ├── authController.js
│ │ ├── productController.js
│ │ └── reviewController.js
│ ├── routes/ # Routing
│ │ ├── auth.js
│ │ ├── products.js
│ │ └── reviews.js
│ ├── middlewares/ # Middleware
│ │ ├── auth.js # JWT Certification
│ │ ├── errorHandler.js
│ │ └── validate.js
│ ├── validators/ # Request for a Trial Certificate
│ │ └── schemas.js
│ └── utils/ # Tools
│ ├── logger.js
│ └── jwt.js
└── README.md
(2) package.json
{
"name": "shophub-reviews",
"version": "1.0.0",
"scripts": {
"start": "node src/app.js",
"dev": "nodemon src/app.js",
"test": "jest --watch"
},
"dependencies": {
"express": "^4.19.2",
"mongoose": "^7.6.0",
"jsonwebtoken": "^9.0.0",
"bcrypt": "^5.1.0",
"joi": "^17.13.0",
"dotenv": "^16.4.0",
"cors": "^2.8.5"
},
"devDependencies": {
"nodemon": "^3.1.0",
"jest": "^29.7.0"
}
}
(3) .env file
NODE_ENV=development
PORT=3000
MONGODB_URI=mongodb://localhost:27017/shophub
JWT_SECRET=your-super-secret-key-change-in-prod
JWT_EXPIRES_IN=7d
3. Module 2: User and Product Models
Data Model Design Principles: Schema design is not simply a matter of “copying table fields into Mongoose”; rather, it involves considering: query patterns (what are the most common queries?), data relationships (which entities need to be associated?), performance requirements (are indexes needed? Should select:false be set for certain fields?), and security requirements (should passwords be masked? Is soft deletion needed?).
Schema Design Decisions:
| Design Decisions | Choice | Reason |
|---|---|---|
| Password Storage | passwordHash + select:false | Not returned by default queries to prevent disclosure |
| Password Hashing | pre-save middleware + bcrypt | Automatic hashing; transparent to business logic |
| Role Design | enum + RBAC | Three roles: customer/admin/moderator |
| Product Rating | Redundant fields: rating + reviewCount | Avoid calculating the aggregate each time |
| Comment Structure | parentId Reference | Supports unlimited nesting levels |
| Soft Delete | isDeleted + pre-find filtering | Data is recoverable; compliance requirements |
| Timestamps | timestamps: true | Automatic management of createdAt/updatedAt |
Model Relationships and Reference Directions: A Review references a User and a Product (many-to-one), and a Review references itself (self-reference enables nesting). The reference direction is "the many-to-one side references the one-to-many side"—that is, instead of embedding an array of Reviews within a Product (which would result in infinite growth), a Review stores a productId.
graph LR
User1[Alice] -->|userId| R1[Review: "Great!"]
User2[Bob] -->|userId| R2[Review: "Good"]
User3[Charlie] -->|userId| R3[Reply: "Thanks!"]
Prod1[Product: Phone] -->|productId| R1
Prod1 -->|productId| R2
R1 -->|parentId| R3
style User1 fill:#cce5ff
style Prod1 fill:#d4edda
style R1 fill:#fff3cd
(1) User Model (including password hashing middleware)
// models/User.js
const mongoose = require('mongoose');
const bcrypt = require('bcrypt');
const UserSchema = new mongoose.Schema({
email: {
type: String,
required: [true, 'Email is required'],
unique: true,
lowercase: true,
trim: true,
match: [/^\S+@\S+\.\S+$/, 'Invalid email format']
},
username: {
type: String,
required: true,
unique: true,
minlength: 3,
maxlength: 30,
match: [/^[a-zA-Z0-9_]+$/, 'Alphanumeric + underscore only']
},
passwordHash: {
type: String,
required: true,
minlength: 60, // bcrypt hash Length
select: false
},
role: {
type: String,
enum: ['customer', 'admin', 'moderator'],
default: 'customer'
},
isActive: { type: Boolean, default: true }
}, { timestamps: true });
// Password Hashing Middleware
UserSchema.pre('save', async function(next) {
if (!this.isModified('passwordHash')) return next();
this.passwordHash = await bcrypt.hash(this.passwordHash, 10);
next();
});
// Confirm Password
UserSchema.methods.comparePassword = function(candidate) {
return bcrypt.compare(candidate, this.passwordHash);
};
module.exports = mongoose.model('User', UserSchema);
(2) Product Model
// models/Product.js
const ProductSchema = new mongoose.Schema({
sku: { type: String, required: true, unique: true, index: true },
title: { type: String, required: true, maxlength: 200 },
description: { type: String, maxlength: 5000 },
price: { type: mongoose.Schema.Types.Decimal128, required: true, min: 0 },
category: { type: String, enum: ['Electronics', 'Books', 'Clothing', 'Home'], index: true },
stock: { type: Number, default: 0, min: 0 },
rating: { type: Number, default: 0, min: 0, max: 5 },
reviewCount: { type: Number, default: 0 },
isActive: { type: Boolean, default: true, index: true }
}, { timestamps: true });
ProductSchema.index({ category: 1, price: -1 });
ProductSchema.index({ title: 'text', description: 'text' });
module.exports = mongoose.model('Product', ProductSchema);
(3) Review Model (including nested replies)
// models/Review.js
const ReviewSchema = new mongoose.Schema({
productId: { type: mongoose.Schema.Types.ObjectId, ref: 'Product', required: true, index: true },
userId: { type: mongoose.Schema.Types.ObjectId, ref: 'User', required: true },
content: { type: String, required: true, maxlength: 1000 },
rating: { type: Number, required: true, min: 1, max: 5 },
parentId: { type: mongoose.Schema.Types.ObjectId, ref: 'Review', default: null, index: true },
likes: [{ type: mongoose.Schema.Types.ObjectId, ref: 'User' }],
likeCount: { type: Number, default: 0 },
isApproved: { type: Boolean, default: true },
isDeleted: { type: Boolean, default: false }
}, { timestamps: true });
ReviewSchema.index({ productId: 1, createdAt: -1 });
ReviewSchema.pre(/^find/, function(next) {
this.where({ isDeleted: { $ne: true } });
next();
});
module.exports = mongoose.model('Review', ReviewSchema);
4. Module 3: Comments CRUD API (Nested Comments)
Additional Notes on the Design Principles of Nested Comments: The query strategy for the reference model (parentId) requires balancing “number of queries” and “data integrity”—the two-query method (top-level comments + replies) requires only 2 database I/O operations but supports only two levels of nesting; the recursive query method ($graphLookup) supports an unlimited number of levels but has poor performance. This system opts for the two-query method combined with in-memory assembly because: 1. Most users only view two levels of comments (top-level + replies); 2. Deeper replies can be loaded on demand via the “View More Replies” button; 3. The performance of the two-query method is far superior to that of the recursive query.
Comment Security Policy Design: Comment systems face three types of security threats—1. Content security: spam comments, sensitive keywords, and advertising links (protection: rate limiting + sensitive keyword filtering + manual moderation); 2. Permission Security: Non-authors editing or deleting others’ comments (Protection: authentication + user ID matching); 3. Injection Security: $where injection, $regex DoS (Protection: parameterized queries + regular expression escaping). Security measures should be implemented uniformly at the middleware layer, rather than being duplicated in each Controller.
graph TB
subgraph "Nested Comments (Threaded Comments)"
A[Review<br/>_id: ObjectId] --> B[Reply 1<br/>parentId: A._id]
A --> C[Reply 2<br/>parentId: A._id]
B --> D[Reply to Reply<br/>parentId: B._id]
C --> E[Reply to Reply<br/>parentId: C._id]
end
style A fill:#d4edda
style B fill:#fff3cd
style C fill:#fff3cd
style D fill:#f8d7da
style E fill:#f8d7da
The core challenge of nested comments (Threaded Comments) is how to represent the "reply" relationship. Two approaches: (1) Embedded documents (embed replies array within Review) -- simple but limited by the 16MB BSON limit, and deep replies are hard to paginate; (2) Reference pattern (parentId points to the parent comment) -- flexible, supports unlimited nesting, supports pagination, but requires additional queries to assemble the tree structure. This project uses the reference pattern.
Nested Comment Query Strategy:
sequenceDiagram
Client Participant
participant API as /products/:id/reviews
participant DB as MongoDB
Client->>API: GET /products/123/reviews
API->>DB: Query top-level comments (parentId=null)
DB-->>API: Returns [R1, R2, R3]
API->>DB: Query all replies (parentId in [R1, R2, R3]._id)
DB-->>API: Returns [R1.1, R1.2, R2.1]
API->>API: Building a tree structure<br/>R1.replies=[R1.1, R1.2]<br/>R2.replies=[R2.1]
API-->>Client: Returns tree-structured data
Review CRUD API Design:
| Operation | Endpoint | Method | Auth | Business Rules |
|---|---|---|---|---|
| View Reviews | /products/:id/reviews | GET | No | Pagination + nested replies |
| Post Review | /products/:id/reviews | POST | Yes | Verify product exists + rating 1-5 |
| Post Reply | /products/:id/reviews | POST | Yes | Verify parent review exists |
| Edit Review | /reviews/:id | PUT | Yes (author) | Only the author can edit |
| Delete Review | /reviews/:id | DELETE | Yes (author/admin) | Soft delete isDeleted |
| Like/Unlike | /reviews/:id/like | POST | Yes | Toggle with $addToSet/$pull |
The like feature uses $addToSet (idempotent add) + $pull (remove) instead of push (which could cause duplicates). It also maintains a likeCount redundant field to avoid counting likes each time.
(1) Create Review
// controllers/reviewController.js
exports.createReview = async (req, res) => {
const { productId } = req.params;
const { content, rating, parentId } = req.body;
// Verify that the product exists
const product = await Product.findById(productId);
if (!product) return res.status(404).json({ error: 'Product not found' });
// If this is a reply, verify that the parent comment exists
if (parentId) {
const parent = await Review.findById(parentId);
if (!parent) return res.status(404).json({ error: 'Parent review not found' });
}
const review = await Review.create({
productId,
userId: req.user._id,
content,
rating,
parentId: parentId || null
});
// Update product rating statistics
await updateProductRating(productId);
await review.populate('userId', 'username avatar');
res.status(201).json(review);
};
(2) Nested Query
exports.listReviews = async (req, res) => {
const { productId } = req.params;
const { sort = 'createdAt', order = 'desc', limit = 20, page = 1 } = req.query;
// Query top-level comments
const reviews = await Review.find({
productId,
parentId: null
})
.populate('userId', 'username avatar')
.sort({ [sort]: order === 'desc' ? -1 : 1 })
.limit(limit * 1)
.skip((page - 1) * limit)
Continue
// View all replies
const reviewIds = reviews.map(r => r._id);
const replies = await Review.find({
parentId: { $in: reviewIds }
})
.populate('userId', 'username avatar')
.sort({ createdAt: 1 })
Continue
// Build a tree structure
const tree = reviews.map(parent => ({
...parent,
replies: replies.filter(r => r.parentId.toString() === parent._id.toString())
}));
res.json({ data: tree, page, limit });
};
(3) Like Feature
exports.likeReview = async (req, res) => {
const { reviewId } = req.params;
const userId = req.user._id;
const review = await Review.findById(reviewId);
if (!review) return res.status(404).json({ error: 'Review not found' });
const alreadyLiked = review.likes.some(id => id.toString() === userId.toString());
if (alreadyLiked) {
await Review.updateOne(
{ _id: reviewId },
{ $pull: { likes: userId }, $inc: { likeCount: -1 } }
);
return res.json({ liked: false });
} else {
await Review.updateOne(
{ _id: reviewId },
{ $addToSet: { likes: userId }, $inc: { likeCount: 1 } }
);
return res.json({ liked: true });
}
};
5. Module 4: Aggregation Analysis
Aggregation Pipeline Design Pattern Details: The review system's aggregation requirements fall into three categories: 1. Single-dimension statistics (rating distribution, review count): a single pipeline with $match + $group/$bucket; 2. Multi-dimension parallel statistics (product detail page overview + distribution + recent reviews): must use $facet to return in a single query; 3. Cross-collection linked statistics (popular product rankings with product info): $group + $lookup + $unwind combination. Design principle: first $match to filter and reduce data volume, then $group to aggregate, finally $sort/$limit to sort and limit.
$facet Sub-pipeline Design Strategy: Each sub-pipeline in $facet should be as lean as possible: 1. The total count sub-pipeline only needs $count, with minimal overhead; 2. The average sub-pipeline uses $group({ _id: null }), producing a single output; 3. The distribution sub-pipeline uses $group({ _id: '$rating' }), with output count equal to the value domain size; 4. The list sub-pipeline requires $sort + $limit + $lookup, with the highest overhead and should be placed last. Sub-pipeline order does not affect execution (parallel), but it does affect code readability -- it is recommended to arrange them from least to most expensive.
graph LR
subgraph "Aggregation Pipeline"
A[Raw Data<br/>1M reviews] -->|"$match"| B[Filtered Data<br/>100K reviews]
B -->|"$group"| C[Aggregated<br/>grouped]
C -->|"$sort + $limit"| D[Top Results<br/>top 10]
end
style A fill:#f8d7da
style B fill:#fff3cd
style C fill:#d4edda
style D fill:#d4edda
The review system needs multi-dimensional statistics -- rating distribution (how many 5-star/4-star), average rating, popular product rankings, recent review trends. If these statistics are computed in application-layer code, large amounts of data must be transferred to Node.js for processing; with aggregation pipelines, the computation is done at the database layer and only the results are returned -- the performance difference can be up to 100x.
Aggregator Design Pattern:
| Statistical Requirements | Aggregation Phase | Output |
|---|---|---|
| Score Distribution | $match → $bucket | [{_id: 5, count: 120}, ...] |
| Product Stats | $match → $facet | {total, avgRating, distribution, recent} |
| Popular Products | $match → $group → $sort → $lookup → $limit | [{productId, avgRating, reviewCount}] |
The Value of $facet: $facet allows multiple aggregation pipelines to run in parallel on the same input—a single query returns four dimensions: total, avgRating, ratingDistribution, and recentReviews. Without $facet, four separate queries would be required.
graph TB
Input[Comment Data Stream] --> Facet["$facet<br/>Multi-channel parallel processing"]
Facet --> P1["Pipeline1: $count<br/>Total"]
Facet --> P2["Pipeline2: $group<br/>Average Rating"]
Facet --> P3["Pipeline3: $group + $sort<br/>Score Distribution"]
Facet --> P4["Pipeline4: $sort + $limit + $lookup<br/>Recent Comments(Contains user information)"]
P1 --> Output[Combined Output<br/>{total, avg, distribution, recent}]
P2 --> Output
P3 --> Output
P4 --> Output
style Facet fill:#d4edda
style Output fill:#cce5ff
How the $bucket Score Distribution Works: $bucket divides scores into buckets based on boundaries—where boundaries=[1,2,3,4,5,6] represents 5 buckets: [1,2), [2,3), [3,4), [4,5), [5,6). The count and average for each bucket are calculated.
(1) Score Distribution ($bucket)
exports.getRatingDistribution = async (req, res) => {
const { productId } = req.params;
const distribution = await Review.aggregate([
{ $match: { productId: new mongoose.Types.ObjectId(productId), parentId: null } },
{
$bucket: {
groupBy: '$rating',
boundaries: [1, 2, 3, 4, 5, 6],
default: 'Other',
output: {
count: { $sum: 1 },
avgHelpful: { $avg: '$likeCount' }
}
}
}
]);
res.json({ data: distribution });
};
(2) Product Statistics ($facet)
exports.getProductStats = async (req, res) => {
const { productId } = req.params;
const stats = await Review.aggregate([
{ $match: { productId: new mongoose.Types.ObjectId(productId), parentId: null } },
{
$facet: {
total: [{ $count: 'count' }],
avgRating: [{ $group: { _id: null, avg: { $avg: '$rating' } } }],
ratingDistribution: [
{ $group: { _id: '$rating', count: { $sum: 1 } } },
{ $sort: { _id: 1 } }
],
recentReviews: [
{ $sort: { createdAt: -1 } },
{ $limit: 5 },
{
$lookup: {
from: 'users',
localField: 'userId',
foreignField: '_id',
as: 'userInfo'
}
},
{ $unwind: '$userInfo' },
{
$project: {
content: 1,
rating: 1,
createdAt: 1,
username: '$userInfo.username',
avatar: '$userInfo.avatar'
}
}
]
}
}
]);
res.json(stats[0]);
};
(3) Top Products
exports.getTopProducts = async (req, res) => {
const { limit = 10 } = req.query;
const topProducts = await Review.aggregate([
{ $match: { parentId: null, isApproved: true } },
{
$group: {
_id: '$productId',
avgRating: { $avg: '$rating' },
reviewCount: { $sum: 1 },
totalLikes: { $sum: '$likeCount' }
}
},
{ $sort: { avgRating: -1, reviewCount: -1 } },
{ $limit: limit * 1 },
{
$lookup: {
from: 'products',
localField: '_id',
foreignField: '_id',
as: 'product'
}
},
{ $unwind: '$product' },
{
$project: {
sku: '$product.sku',
title: '$product.title',
thumbnail: '$product.thumbnail',
avgRating: 1,
reviewCount: 1
}
}
]);
res.json({ data: topProducts });
};
6. Module 5: Permissions and Security
A Detailed Explanation of the Defense-in-Depth Principle: API security is not a single point of defense but a multi-layered defense system—the network layer (HTTPS + CORS) prevents eavesdropping and cross-origin abuse; the application layer (JWT authentication + RBAC authorization + input validation) intercepts unauthorized and malicious requests; and the data layer (parameterized queries + database users with minimal privileges) prevents injection attacks and unauthorized access. The significance of independent defense at each layer: A breach in any single layer does not compromise the protection provided by the other layers—even if CORS is misconfigured, JWT authentication can still block unauthenticated users; even if a JWT is compromised, RBAC can still restrict the scope of operations for users with low privileges.
JWT Token Security Best Practices: 1. Key Strength: Generate a 256-bit random key using openssl rand -hex 32; 2. Expiration Time: 7 days (to balance security and user experience; this can be shortened for sensitive operations); 3. Token Refresh: Short expiration for access tokens + long expiration for refresh tokens (dual-token mechanism); 4. Token Storage: Use httpOnly cookies (to prevent XSS) in the frontend instead of localStorage; 5. Token Revocation: Maintain a blacklist (Redis SET) or use short expiration times to reduce the need for revocation.
graph TB
subgraph "Defense in Depth"
L1[Network Layer<br/>TLS + CORS] --> L2[Application Layer<br/>Auth + Authorization + Input Validation]
L2 --> L3[Data Layer<br/>Parameterized Queries + Least Privilege]
end
style L1 fill:#d4edda
style L2 fill:#fff3cd
style L3 fill:#f8d7da
API security follows the "defense in depth" principle -- not a single line of defense, but multiple layers: Network layer (TLS/CORS) → Application layer (authentication + authorization + input validation) → Data layer (parameterized queries + least privilege). Each layer defends independently; if one layer is breached, the others are not affected.
JWT Authentication vs Session Authentication:
| Dimension | JWT | Session |
|---|---|---|
| Storage Location | Client (Token) | Server (Memory/Redis) |
| Scalability | Stateless, naturally supports distributed | Requires shared Session storage |
| Security | Token leak cannot be instantly revoked | Can instantly destroy Session |
| Performance | No server-side query overhead | Queries Session storage each time |
| Expiration Management | exp claim, cannot proactively renew | Can be renewed with sliding expiration |
| Suitable Scenarios | API / Microservices | Traditional Web Applications |
RBAC Permission Model: Role-Based Access Control assigns permissions based on roles -- users have roles, and roles have permissions. This system has three roles:
| Role | Permissions | Scope |
|---|---|---|
| customer | Review, like, edit own reviews | /reviews (own) |
| moderator | Review/delete any reviews | /reviews (all) + moderation dashboard |
| admin | Manage products, users, all reviews | /products + /users + /reviews (all) |
Injection Protection Essentials: MongoDB injection risks mainly come from $where (executes arbitrary JS) and unanchored $regex (DoS attacks). Protection principles: (1) Always use parameterized queries instead of string concatenation; (2) Mongoose automatically escapes query values; (3) $regex requires escaping special characters.
sequenceDiagram
Client Participant
participant Auth as authenticate Middleware
participant Authorize as authorize Middleware
participant Controller
DB participant
Client->>Auth: Request + Bearer Token
Auth->>Auth: jwt.verify(token, secret)
alt Token is invalid
Auth-->>Client: 401 Unauthorized
else Token Valid
Auth->>Authorize: req.user = {id, role}
Authorize->>Authorize: roles.includes(req.user.role)?
alt: No permission
Authorize-->>Client: 403 Forbidden
else has permission
Authorize->>Controller: Execute business logic
Controller >> DB: Parameterized Queries
DB-->>Client: 200 OK
than
than
(1) JWT Middleware
// middlewares/auth.js
const jwt = require('jsonwebtoken');
exports.authenticate = (req, res, next) => {
const token = req.header('Authorization')?.replace('Bearer ', '');
if (!token) return res.status(401).json({ error: 'No token' });
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET);
req.user = decoded;
next();
} catch (err) {
res.status(401).json({ error: 'Invalid token' });
}
};
exports.authorize = (...roles) => (req, res, next) => {
if (!req.user || !roles.includes(req.user.role)) {
return res.status(403).json({ error: 'Forbidden' });
}
next();
};
(2) Authentication API
// controllers/authController.js
const User = require('../models/User');
const jwt = require('jsonwebtoken');
exports.register = async (req, res) => {
const { email, username, password } = req.body;
const existing = await User.findOne({ $or: [{ email }, { username }] });
if (existing) return res.status(409).json({ error: 'Email or username already exists' });
const user = await User.create({
email,
username,
passwordHash: password // pre-save The middleware performs hashing
});
const token = jwt.sign(
{ id: user._id, role: user.role },
process.env.JWT_SECRET,
{ expiresIn: '7d' }
);
res.status(201).json({ user: { id: user._id, email: user.email, username: user.username }, token });
};
exports.login = async (req, res) => {
const { email, password } = req.body;
const user = await User.findOne({ email }).select('+passwordHash');
if (!user) return res.status(401).json({ error: 'Invalid credentials' });
const valid = await user.comparePassword(password);
if (!valid) return res.status(401).json({ error: 'Invalid credentials' });
const token = jwt.sign(
{ id: user._id, role: user.role },
process.env.JWT_SECRET,
{ expiresIn: '7d' }
);
res.json({ user: { id: user._id, email: user.email, username: user.username, role: user.role }, token });
};
(3) Injection Protection
// ❌ Danger: $where executes arbitrary JavaScript
db.reviews.find({ $where: 'this.userId == "' + userId + '"' });
// ✅ Security: Use parameterized queries
db.reviews.find({ userId: new ObjectId(userId) });
// ✅ Mongoose auto-escapes
const reviews = await Review.find({ userId: userId });
// ❌ Danger: $regex DoS
db.reviews.find({ content: { $regex: req.query.q } });
// ✅ Security: Escape special characters in regular expressions
function escapeRegex(str) {
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
db.reviews.find({ content: { $regex: escapeRegex(req.query.q), $options: 'i' } });
7. Module 6: Deployment
Deployment Architecture Decision: The choice of deployment strategy is based on project scale and team capability: 1. Atlas + Render (this project's choice): zero-ops startup, suitable for small-to-medium projects and MVP stages, monthly cost < $50; 2. Self-hosted MongoDB + Docker: fully controllable but requires DevOps capability, suitable for mid-to-large projects with a DevOps team; 3. Kubernetes + Atlas: elastic scaling + managed database, suitable for production systems with fluctuating traffic. This project chooses Atlas + Render for the core reason: to focus limited energy on business development rather than operations.
Health Check and Auto-Recovery: Production deployment health checks are not just about /healthz returning 200 -- they need to check three layers of health: 1. Application layer (Is Express responsive?); 2. Database layer (Is MongoDB reachable?); 3. Dependency layer (Are Redis/ES functioning normally?). The health check endpoint is periodically called by the load balancer; after N consecutive failures, the instance is automatically removed and restarted. When the database connection is lost, it should return 503 (Service Unavailable) instead of 200, to avoid sending traffic to unhealthy instances.
graph TB
subgraph "Deployment Architecture"
A[Client] --> B[Render<br/>Node.js App]
B --> C[Atlas<br/>MongoDB Cluster]
B --> D[CDN<br/>Static Assets]
end
style A fill:#d4edda
style B fill:#fff3cd
style C fill:#d4edda
style D fill:#fff3cd
Deployment is not just "git push and done" -- it requires considering: environment isolation (dev/staging/prod), secret management (not storing passwords in code), health checks (auto-restart unhealthy instances), and data security (TLS encryption + backup and recovery). This project's deployment strategy: Atlas managed database + Render/Railway managed application -- zero-ops startup, suitable for small-to-medium projects.
Deployment Architecture:
graph LR
Client[User's browser] -->|HTTPS| CDN[CDN / Static Resources]
Client -->|HTTPS| LB[Load Balancer]
LB -->|HTTP| App1[Render Instance 1<br/>Express App]
LB -->|HTTP| App2[Render Instance 2<br/>Express App]
App1 -->|TLS + SRV| Atlas[MongoDB Atlas<br/>3 Node Replica Set]
App2 -->|TLS + SRV| Atlas
Atlas -->| PITR | Backup[Automatic Backup<br/>7Day Reserved]
style Atlas fill:#d4edda
style App1 fill:#cce5ff
Environment Configuration Policy:
| Environment Variable | Development Value | Production Value | Management Method |
|---|---|---|---|
| NODE_ENV | development | production | .env / Platform settings |
| MONGODB_URI | mongodb://localhost:27017 | mongodb+srv://... | Platform Key Management |
| JWT_SECRET | test-secret | 256-bit random string | openssl rand -hex 32 |
| CORS_ORIGIN | * | https://shophub.example.com | .env / Platform settings |
| PORT | 3000 | Platform Assignment | Default is fine |
Health Check Design: The /healthz endpoint not only checks whether Express is running, but also verifies that MongoDB is reachable—if the database connection is lost, the application should return a 503 (Service Unavailable) status code instead of a 200, so that the load balancer can automatically remove unhealthy instances.
(1) MongoDB Atlas Configuration
# 1. Create Atlas Cluster(Recommended Courses #01)
# 2. Layout IP Whitelist:0.0.0.0/0(Development)or application server IP
# 3. Create a Database User:app_user / <password>
# 4. Get the connection string:
mongodb+srv://app_user:<password>@cluster0.mongodb.net/shophub?retryWrites=true&w=majority
(2) Environment Variables (Production)
# .env.production
NODE_ENV=production
PORT=3000
MONGODB_URI=mongodb+srv://app_user:StrongPass@cluster0.mongodb.net/shophub?retryWrites=true&w=majority
JWT_SECRET=<generated-strong-secret-256-bit>
JWT_EXPIRES_IN=7d
CORS_ORIGIN=https://shophub.example.com
(3) Health Checkup
app.get('/healthz', async (req, res) => {
try {
const db = mongoose.connection.db;
await db.admin().command({ ping: 1 });
res.json({
status: 'ok',
uptime: process.uptime(),
mongo: 'connected',
timestamp: new Date().toISOString()
});
} catch (err) {
res.status(503).json({ status: 'error', error: err.message });
}
});
(4) Render / Railway Deployment
# === Render Deployment ===
# 1. Connect GitHub Warehouse
# 2. Set Environment Variables (MONGODB_URI, JWT_SECRET, etc.)
# 3. Set Up Build Commands:npm install
# 4. Set the startup command:npm start
# 5. Automatic HTTPS + Deployment
# === Railway Deployment ===
railway login
railway init
railway add mongodb # Add with One Click MongoDB
railway up
(5) Performance Optimization Checklist
// === src/app.js Optimized Version ===
const mongoose = require('mongoose');
mongoose.connect(process.env.MONGODB_URI, {
maxPoolSize: 50,
minPoolSize: 5,
serverSelectionTimeoutMS: 5000
});
app.use(express.json({ limit: '1mb' }));
app.use(cors({
origin: process.env.CORS_ORIGIN || '*',
credentials: true
}));
8. Complete Project Demonstration
Project Startup and Testing Process: A complete project demonstration follows the process of "Environment Setup → Start Services → Test APIs → Verify Functionality → Deployment and Launch." First, start the MongoDB replica set (transactions and Change Streams require a replica set), then start the Express application, and finally use curl to test the core API endpoints.
API Test Checklist:
| # | Feature | Endpoint | Expected Status Code |
|---|---|---|---|
| 1 | Register | POST /api/auth/register | 201 |
| 2 | Login | POST /api/auth/login | 200 |
| 3 | Create Product | POST /api/products | 201 (admin) |
| 4 | Product List | GET /api/products | 200 |
| 5 | Post a Review | POST /api/products/:id/reviews | 201 |
| 6 | List of Reviews | GET /api/products/:id/reviews | 200 |
| 7 | Like | POST /api/reviews/:id/like | 200 |
| 8 | Product Statistics | GET /api/products/:id/stats | 200 |
| 9 | Health Check | GET /healthz | 200 |
# === Launch the Project ===
npm install
npm start
# === API Usage Example ===
# 1. Registered Users
curl -X POST http://localhost:3000/api/auth/register \
-H "Content-Type: application/json" \
-d '{"email":"alice@example.com","username":"alice","password":"Pass123!"}'
# 2. Log In
curl -X POST http://localhost:3000/api/auth/login \
-H "Content-Type: application/json" \
-d '{"email":"alice@example.com","password":"Pass123!"}'
# 3. Create a Review
curl -X POST http://localhost:3000/api/products/<productId>/reviews \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{"content":"Great product!","rating":5}'
# 4. View Product Statistics
curl http://localhost:3000/api/products/<productId>/stats
# 5. Health Checkup
curl http://localhost:3000/healthz
▶ Example 1: Demo of Nested Comments + Like Feature
// === Scenario: Alice reviews a product, Bob replies to Alice, Charlie likes it ===
// 1. Alice Published 5 Star Reviews
const aliceReview = await Review.create({
productId: '647f1f77bcf86cd799439011',
userId: aliceId,
content: 'Excellent smartphone! The camera quality is outstanding.',
rating: 5,
parentId: null // Top Comments
});
// Back: { _id: 'review001', content: '...', rating: 5, likeCount: 0 }
// 2. Bob Reply Alice
const bobReply = await Review.create({
productId: '647f1f77bcf86cd799439011',
userId: bobId,
content: 'I agree, the camera is amazing!',
rating: 5,
parentId: aliceReview._id // Quote parent's comment
});
// Back: { _id: 'review002', parentId: 'review001', content: '...' }
// 3. Charlie likes Alice's comment
await Review.updateOne(
{ _id: aliceReview._id },
{ $addToSet: { likes: charlieId }, $inc: { likeCount: 1 } }
);
// Alice Comments on: { likeCount: 1, likes: [charlieId] }
// 4. Like again → Cancel
await Review.updateOne(
{ _id: aliceReview._id },
{ $pull: { likes: charlieId }, $inc: { likeCount: -1 } }
);
// Alice Comments on: { likeCount: 0, likes: [] }
// 5. Querying a Nested Comment Tree
const reviews = await Review.find({ productId: '647f...', parentId: null })
.populate('userId', 'username avatar')
.lean();
const replies = await Review.find({ parentId: { $in: reviews.map(r => r._id) } })
.populate('userId', 'username avatar')
.lean();
const tree = reviews.map(r => ({
...r,
replies: replies.filter(rep => rep.parentId.toString() === r._id.toString())
}));
console.log(JSON.stringify(tree, null, 2));
// [{
// content: 'Excellent smartphone!...',
// userId: { username: 'alice', avatar: '...' },
// replies: [{ content: 'I agree...', userId: { username: 'bob' } }]
// }]
Output: A nested comment tree structure: Alice comments → Bob replies, Charlie likes/unlikes.
▶ Example 2: Live Demo of the Complete ShopHub Review System
# === 1. Start MongoDB Dungeon Collection ===
docker run -d --name mongo -p 27017:27017 mongo:7.0 --replSet rs0
docker exec mongo mongosh --eval 'rs.initiate()'
# === 2. Start Node.js Applications ===
npm install
npm start
# Output:
# ✅ MongoDB connected: localhost
# 🚀 Server running on port 3000
# === 3. Test Core API ===
# 3.1 User Registration
curl -X POST http://localhost:3000/api/auth/register \
-H "Content-Type: application/json" \
-d '{"email":"alice@example.com","username":"alice","password":"Pass123!"}'
# Back:
# {
# "success": true,
# "user": { "id": "...", "email": "alice@example.com", "username": "alice" },
# "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
# }
# 3.2 User Login
curl -X POST http://localhost:3000/api/auth/login \
-H "Content-Type: application/json" \
-d '{"email":"alice@example.com","password":"Pass123!"}'
# 3.3 Create a Product
curl -X POST http://localhost:3000/api/products \
-H "Authorization: Bearer <admin_token>" \
-H "Content-Type: application/json" \
-d '{
"sku": "PHONE-001",
"title": "Smartphone X",
"price": 599,
"category": "Electronics",
"stock": 50
}'
# 3.4 Product List(With pagination + Projection)
curl 'http://localhost:3000/api/products?page=1&limit=20&category=Electronics'
# Back:
# {
# "success": true,
# "data": [{ "sku": "PHONE-001", "title": "Smartphone X", "price": 599, "thumbnail": "...", "rating": 0 }],
# "meta": { "page": 1, "limit": 20, "total": 1, "pages": 1 }
# }
# 3.5 Add a comment
curl -X POST http://localhost:3000/api/products/507f1f77bcf86cd799439021/reviews \
-H "Authorization: Bearer <user_token>" \
-H "Content-Type: application/json" \
-d '{"content":"Great phone!","rating":5}'
# 3.6 Like and Comment
curl -X POST http://localhost:3000/api/reviews/507f1f77bcf86cd799439031/like \
-H "Authorization: Bearer <user_token>"
# 3.7 View Product Statistics
curl http://localhost:3000/api/products/507f1f77bcf86cd799439021/stats
# Back:
# {
# "total": [{ "count": 1 }],
# "avgRating": [{ "avg": 5 }],
# "ratingDistribution": [{ "_id": 5, "count": 1 }],
# "recentReviews": [{ "content": "Great phone!", "rating": 5, "username": "alice" }]
# }
# 3.8 Health Checkup
curl http://localhost:3000/healthz
# Back:
# {
# "status": "ok",
# "uptime": 1234,
# "mongo": "connected",
# "timestamp": "2026-07-06T10:00:00.000Z"
# }
# === 4. Deploy to Render/Railway ===
git push heroku main
# Automatic Deployment,Environment Variables MONGODB_URI Orientation Atlas
# === 5. Performance Monitoring(Production)===
# Datadog APM Automatic Tracking:
# - mongoose Query Duration
# - API Response Time
# - Number of database connections
# - Slow Query Alerts
Output: A complete e-commerce review system demonstrating the entire workflow—from registration, login, product management, review CRUD operations, liking, and statistics to health checks—that can be deployed directly to a production environment.
❓ FAQ
📖 Summary
- Complete E-commerce Review System Architecture (Node.js + Express + Mongoose)
- 6 Modules: Initialization → Model → CRUD → Aggregate Analysis → Permissions → Deployment
- Nested comments (parent_id reference)
- Aggregate statistics ($bucket + $facet)
- JWT Authentication + RBAC Roles
- MongoDB Atlas Cloud Deployment
📝 Exercises
- Basic Question (⭐): Set up the project skeleton (including package.json, .env, and app.js).
- Basic Problems (⭐): Implement the three models: User, Product, and Review.
- Advanced Exercise (⭐⭐): Implement CRUD operations for comments (create, list, like, soft delete).
- Advanced Exercise (⭐⭐): Implement aggregate statistics (rating distribution + product statistics + top charts).
- Challenge (⭐⭐⭐): Deploy a complete e-commerce review system to Atlas or Render, including all features of the six modules.



