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.

100%
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:

100%
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

BASH
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

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

BASH
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.

100%
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)

JAVASCRIPT
// 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

JAVASCRIPT
// 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)

JAVASCRIPT
// 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.

100%
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:

100%
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

JAVASCRIPT
// 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

JAVASCRIPT
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

JAVASCRIPT
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.

100%
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.

100%
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)

JAVASCRIPT
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)

JAVASCRIPT
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

JAVASCRIPT
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.

100%
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.

100%
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

JAVASCRIPT
// 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

JAVASCRIPT
// 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

JAVASCRIPT
// ❌ 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.

100%
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:

100%
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

BASH
# 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)

BASH
# .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

JAVASCRIPT
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

BASH
# === 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

JAVASCRIPT
// === 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
BASH
# === 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

JAVASCRIPT
// === 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

BASH
# === 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

Q How can we further optimize the project after it’s completed?
A Caching popular products in Redis, full-text search with Elasticsearch, using a CDN for static resources, and containerized deployment with K8s.
Q What are the anti-spam measures in the comment system?
A Comment frequency limits, sensitive word filtering, a user reporting system, and a manual moderation backend.
Q How do I set up comment notifications?
A Use Change Streams to monitor changes to comments and trigger email or push notifications.

📖 Summary


📝 Exercises

  1. Basic Question (⭐): Set up the project skeleton (including package.json, .env, and app.js).
  2. Basic Problems (⭐): Implement the three models: User, Product, and Review.
  3. Advanced Exercise (⭐⭐): Implement CRUD operations for comments (create, list, like, soft delete).
  4. Advanced Exercise (⭐⭐): Implement aggregate statistics (rating distribution + product statistics + top charts).
  5. Challenge (⭐⭐⭐): Deploy a complete e-commerce review system to Atlas or Render, including all features of the six modules.
Web-Tutorial.com

Web-Tutorial Tech Team

A team of developers maintaining programming tutorials. Each tutorial is written and reviewed by developers with expertise in that field. We work to keep our content accurate and reliable — if you spot an issue, please let us know.

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏