/web-app-features

How to Add User Reviews & Ratings to Your Web App

Learn how to easily add user reviews & ratings to your web app for better engagement and trust. Step-by-step guide included!

Book a free  consultation
4.9
Clutch rating 🌟
600+
Happy partners
17+
Countries served
190+
Team members
Matt Graham, CEO of Rapid Developers

Book a call with an Expert

Starting a new venture? Need to upgrade your web app? RapidDev builds application with your growth in mind.

How to Add User Reviews & Ratings to Your Web App

Adding User Reviews & Ratings to Your Web App: A Developer's Guide

 

Why Reviews & Ratings Matter

 

Implementing a review system isn't just a feature checkbox—it's a business multiplier. Reviews build trust (92% of consumers read reviews before purchasing), provide valuable feedback loops, and create user-generated content that boosts SEO. But poorly implemented review systems can become magnets for spam, toxicity, and data headaches.

 

The Architecture Approach

 

Database Structure: Getting the Foundation Right

 

CREATE TABLE reviews (
    id INT AUTO_INCREMENT PRIMARY KEY,
    user_id INT NOT NULL,
    product_id INT NOT NULL,
    rating DECIMAL(2,1) NOT NULL, // Store as decimal for half-star ratings
    review_text TEXT,
    helpful_votes INT DEFAULT 0,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    status ENUM('pending', 'approved', 'rejected') DEFAULT 'pending',
    FOREIGN KEY (user_id) REFERENCES users(id),
    FOREIGN KEY (product_id) REFERENCES products(id),
    UNIQUE KEY (user_id, product_id) // Prevent duplicate reviews
);

// For review media attachments (optional)
CREATE TABLE review_media (
    id INT AUTO_INCREMENT PRIMARY KEY,
    review_id INT NOT NULL,
    media_type ENUM('image', 'video') NOT NULL,
    media_url VARCHAR(255) NOT NULL,
    FOREIGN KEY (review_id) REFERENCES reviews(id) ON DELETE CASCADE
);

 

Core Components to Build

 

  • Review Form Component: The user-facing interface where customers submit ratings and reviews
  • Review Display Component: Shows individual reviews and aggregated ratings
  • Review Management API: Backend endpoints for CRUD operations on reviews
  • Moderation System: Filters for spam, profanity, and content policy violations

 

Frontend Implementation

 

The Review Form: More Than Just Stars

 

// React component example (simplified)
function ReviewForm({ productId, userId }) {
  const [rating, setRating] = useState(0);
  const [reviewText, setReviewText] = useState('');
  const [images, setImages] = useState([]);
  const [isSubmitting, setIsSubmitting] = useState(false);
  const [errors, setErrors] = useState({});
  
  const validateForm = () => {
    const newErrors = {};
    if (rating === 0) newErrors.rating = "Please select a rating";
    if (reviewText.length < 10) newErrors.reviewText = "Review must be at least 10 characters";
    return newErrors;
  };

  const handleSubmit = async (e) => {
    e.preventDefault();
    const validationErrors = validateForm();
    
    if (Object.keys(validationErrors).length > 0) {
      setErrors(validationErrors);
      return;
    }
    
    setIsSubmitting(true);
    
    try {
      // First upload any images
      const uploadedImageUrls = [];
      for (const image of images) {
        const formData = new FormData();
        formData.append('image', image);
        const response = await fetch('/api/upload-image', {
          method: 'POST',
          body: formData
        });
        const data = await response.json();
        uploadedImageUrls.push(data.imageUrl);
      }
      
      // Then submit the review with image URLs
      const reviewResponse = await fetch('/api/reviews', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          productId,
          userId,
          rating,
          reviewText,
          images: uploadedImageUrls
        })
      });
      
      if (!reviewResponse.ok) throw new Error('Failed to submit review');
      
      // Success handling
      setRating(0);
      setReviewText('');
      setImages([]);
      toast.success('Your review has been submitted and is pending approval');
    } catch (error) {
      console.error('Error submitting review:', error);
      toast.error('Failed to submit your review. Please try again.');
    } finally {
      setIsSubmitting(false);
    }
  };
  
  return (
    <form onSubmit={handleSubmit} className="review-form">
      <div className="star-rating">
        {[1, 2, 3, 4, 5].map((star) => (
          <button
            type="button"
            key={star}
            className={star <= rating ? 'star filled' : 'star'}
            onClick={() => setRating(star)}
          >
            ★
          </button>
        ))}
        {errors.rating && <span className="error">{errors.rating}</span>}
      </div>
      
      <textarea
        value={reviewText}
        onChange={(e) => setReviewText(e.target.value)}
        placeholder="Share your experience with this product..."
        rows={4}
      />
      {errors.reviewText && <span className="error">{errors.reviewText}</span>}
      
      <div className="image-upload">
        <input
          type="file"
          accept="image/*"
          multiple
          onChange={(e) => setImages([...images, ...e.target.files])}
        />
        <div className="preview">
          {images.map((image, i) => (
            <div key={i} className="image-preview">
              <img src={URL.createObjectURL(image)} alt="Preview" />
              <button type="button" onClick={() => setImages(images.filter((_, idx) => idx !== i))}>
                Remove
              </button>
            </div>
          ))}
        </div>
      </div>
      
      <button type="submit" disabled={isSubmitting}>
        {isSubmitting ? 'Submitting...' : 'Submit Review'}
      </button>
    </form>
  );
}

 

Displaying Reviews: Beyond Simple Lists

 

// ReviewList component with sorting and filtering
function ReviewList({ productId }) {
  const [reviews, setReviews] = useState([]);
  const [loading, setLoading] = useState(true);
  const [sortBy, setSortBy] = useState('newest');
  const [filterByRating, setFilterByRating] = useState(null);
  const [stats, setStats] = useState({
    averageRating: 0,
    totalReviews: 0,
    ratingCounts: {1: 0, 2: 0, 3: 0, 4: 0, 5: 0}
  });
  
  useEffect(() => {
    async function fetchReviews() {
      setLoading(true);
      try {
        const response = await fetch(
          `/api/reviews?productId=${productId}&sort=${sortBy}${filterByRating ? `&rating=${filterByRating}` : ''}`
        );
        const data = await response.json();
        setReviews(data.reviews);
        setStats(data.stats);
      } catch (error) {
        console.error('Error fetching reviews:', error);
      } finally {
        setLoading(false);
      }
    }
    
    fetchReviews();
  }, [productId, sortBy, filterByRating]);

  // Calculate percentage for star rating bar visualization
  const calculatePercentage = (count) => {
    return stats.totalReviews > 0 ? (count / stats.totalReviews) * 100 : 0;
  };
  
  return (
    <div className="reviews-container">
      {/* Summary section */}
      <div className="review-summary">
        <div className="average-rating">
          <span className="rating-number">{stats.averageRating.toFixed(1)}</span>
          <div className="stars">
            {/* Render stars based on average rating */}
            {[1, 2, 3, 4, 5].map(star => (
              <span key={star} className={stats.averageRating >= star ? 'star filled' : 'star'}>★</span>
            ))}
          </div>
          <div className="total-reviews">Based on {stats.totalReviews} reviews</div>
        </div>
        
        <div className="rating-breakdown">
          {[5, 4, 3, 2, 1].map(rating => (
            <div key={rating} className="rating-bar" onClick={() => setFilterByRating(filterByRating === rating ? null : rating)}>
              <span className="rating-label">{rating} stars</span>
              <div className="bar-container">
                <div 
                  className="bar-fill" 
                  style={{width: `${calculatePercentage(stats.ratingCounts[rating])}%`}}
                ></div>
              </div>
              <span className="rating-count">{stats.ratingCounts[rating]}</span>
            </div>
          ))}
        </div>
      </div>
      
      {/* Controls */}
      <div className="review-controls">
        <select value={sortBy} onChange={(e) => setSortBy(e.target.value)}>
          <option value="newest">Newest First</option>
          <option value="oldest">Oldest First</option>
          <option value="highest">Highest Rated</option>
          <option value="lowest">Lowest Rated</option>
          <option value="helpful">Most Helpful</option>
        </select>
        
        {filterByRating && (
          <div className="active-filter">
            Showing only {filterByRating}-star reviews
            <button onClick={() => setFilterByRating(null)}>Clear Filter</button>
          </div>
        )}
      </div>
      
      {/* Review List */}
      {loading ? (
        <div className="loading">Loading reviews...</div>
      ) : reviews.length === 0 ? (
        <div className="no-reviews">No reviews yet. Be the first to leave a review!</div>
      ) : (
        <div className="review-list">
          {reviews.map(review => (
            <ReviewItem 
              key={review.id} 
              review={review} 
              onMarkHelpful={() => handleMarkHelpful(review.id)}
            />
          ))}
        </div>
      )}
    </div>
  );
}

// Individual review component
function ReviewItem({ review, onMarkHelpful }) {
  return (
    <div className="review-item">
      <div className="review-header">
        <div className="user-info">
          <img src={review.user.avatar || '/default-avatar.png'} alt={review.user.name} />
          <span className="username">{review.user.name}</span>
        </div>
        <div className="review-meta">
          <div className="rating">
            {[1, 2, 3, 4, 5].map(star => (
              <span key={star} className={review.rating >= star ? 'star filled' : 'star'}>★</span>
            ))}
          </div>
          <div className="date">{new Date(review.created_at).toLocaleDateString()}</div>
        </div>
      </div>
      
      <div className="review-content">
        <p>{review.review_text}</p>
        
        {review.media && review.media.length > 0 && (
          <div className="review-media">
            {review.media.map(item => (
              <div key={item.id} className="media-item">
                {item.media_type === 'image' ? (
                  <img src={item.media_url} alt="Review attachment" />
                ) : (
                  <video src={item.media_url} controls />
                )}
              </div>
            ))}
          </div>
        )}
      </div>
      
      <div className="review-footer">
        <button onClick={onMarkHelpful} className="helpful-button">
          <span>Helpful ({review.helpful_votes})</span>
        </button>
      </div>
    </div>
  );
}

 

Backend Implementation

 

RESTful API Design

 

// Express.js API endpoints example (Node.js)
const express = require('express');
const router = express.Router();
const { body, validationResult } = require('express-validator');
const { isAuthenticated, canModerateReviews } = require('../middleware/auth');
const db = require('../database');
const { checkForSpam, filterProfanity } = require('../utils/content-moderation');

// Submit a new review
router.post('/reviews',
  isAuthenticated, // Ensures user is logged in
  [
    body('productId').isInt().withMessage('Valid product ID is required'),
    body('rating').isFloat({ min: 1, max: 5 }).withMessage('Rating must be between 1 and 5'),
    body('reviewText').isLength({ min: 10, max: 2000 }).withMessage('Review must be between 10 and 2000 characters'),
    body('images').isArray().optional(),
  ],
  async (req, res) => {
    // Validate request
    const errors = validationResult(req);
    if (!errors.isEmpty()) {
      return res.status(400).json({ errors: errors.array() });
    }
    
    const { productId, rating, reviewText, images = [] } = req.body;
    const userId = req.user.id; // From auth middleware
    
    // Check if user has already reviewed this product
    const existingReview = await db.query(
      'SELECT id FROM reviews WHERE user_id = ? AND product_id = ?',
      [userId, productId]
    );
    
    if (existingReview.length > 0) {
      return res.status(400).json({ 
        error: 'You have already reviewed this product. You can edit your existing review.' 
      });
    }
    
    // Check if user has purchased the product (optional business rule)
    const hasPurchased = await db.query(
      'SELECT 1 FROM orders o JOIN order_items oi ON o.id = oi.order_id WHERE o.user_id = ? AND oi.product_id = ? LIMIT 1',
      [userId, productId]
    );
    
    // Apply content moderation
    const isSpam = await checkForSpam(reviewText);
    if (isSpam) {
      return res.status(400).json({ error: 'Your review appears to be spam. Please revise it.' });
    }
    
    // Filter profanity but still save the review
    const cleanedReview = filterProfanity(reviewText);
    
    // Determine status based on business rules
    // Auto-approve if they purchased, otherwise require moderation
    const status = hasPurchased.length > 0 ? 'approved' : 'pending';
    
    try {
      // Start a transaction
      await db.beginTransaction();
      
      // Insert the review
      const result = await db.query(
        'INSERT INTO reviews (user_id, product_id, rating, review_text, status) VALUES (?, ?, ?, ?, ?)',
        [userId, productId, rating, cleanedReview, status]
      );
      
      const reviewId = result.insertId;
      
      // Add any images
      if (images.length > 0) {
        const mediaValues = images.map(imageUrl => [reviewId, 'image', imageUrl]);
        await db.query(
          'INSERT INTO review_media (review_id, media_type, media_url) VALUES ?',
          [mediaValues]
        );
      }
      
      // Update product average rating
      await db.query(`
        UPDATE products p 
        SET 
          p.avg_rating = (
            SELECT AVG(rating) FROM reviews 
            WHERE product_id = ? AND status = 'approved'
          ),
          p.review_count = (
            SELECT COUNT(*) FROM reviews 
            WHERE product_id = ? AND status = 'approved'
          )
        WHERE p.id = ?
      `, [productId, productId, productId]);
      
      await db.commit();
      
      res.status(201).json({ 
        message: status === 'approved' 
          ? 'Your review has been published.' 
          : 'Your review has been submitted and is awaiting approval.' 
      });
    } catch (error) {
      await db.rollback();
      console.error('Error submitting review:', error);
      res.status(500).json({ error: 'Failed to submit review' });
    }
  }
);

// Get reviews for a product with filtering and sorting
router.get('/reviews', async (req, res) => {
  try {
    const { 
      productId, 
      page = 1, 
      limit = 10,
      sort = 'newest',
      rating = null,
      verified = false // Only show reviews from verified purchasers
    } = req.query;
    
    const offset = (page - 1) * limit;
    
    // Build the query with proper sorting and filtering
    let query = `
      SELECT 
        r.*, 
        u.name as user_name, 
        u.avatar as user_avatar,
        (SELECT COUNT(*) FROM orders o JOIN order_items oi 
         ON o.id = oi.order_id 
         WHERE o.user_id = r.user_id AND oi.product_id = r.product_id) as is_verified_purchase
      FROM reviews r
      JOIN users u ON r.user_id = u.id
      WHERE r.product_id = ? AND r.status = 'approved'
    `;
    
    const queryParams = [productId];
    
    if (rating) {
      query += ' AND r.rating = ?';
      queryParams.push(rating);
    }
    
    if (verified) {
      query += ' AND is_verified_purchase > 0';
    }
    
    // Add sorting
    switch (sort) {
      case 'newest':
        query += ' ORDER BY r.created_at DESC';
        break;
      case 'oldest':
        query += ' ORDER BY r.created_at ASC';
        break;
      case 'highest':
        query += ' ORDER BY r.rating DESC, r.helpful_votes DESC';
        break;
      case 'lowest':
        query += ' ORDER BY r.rating ASC, r.helpful_votes DESC';
        break;
      case 'helpful':
        query += ' ORDER BY r.helpful_votes DESC, r.created_at DESC';
        break;
      default:
        query += ' ORDER BY r.created_at DESC';
    }
    
    query += ' LIMIT ? OFFSET ?';
    queryParams.push(parseInt(limit), offset);
    
    // Execute the main query
    const reviews = await db.query(query, queryParams);
    
    // Get media for all reviews in one query
    const reviewIds = reviews.map(review => review.id);
    
    if (reviewIds.length > 0) {
      const mediaItems = await db.query(
        'SELECT * FROM review_media WHERE review_id IN (?)',
        [reviewIds]
      );
      
      // Attach media to the correct reviews
      reviews.forEach(review => {
        review.media = mediaItems.filter(item => item.review_id === review.id);
      });
    }
    
    // Get rating statistics
    const stats = await db.query(`
      SELECT 
        COALESCE(AVG(rating), 0) as average_rating,
        COUNT(*) as total_reviews,
        SUM(CASE WHEN rating = 1 THEN 1 ELSE 0 END) as rating_1,
        SUM(CASE WHEN rating = 2 THEN 1 ELSE 0 END) as rating_2,
        SUM(CASE WHEN rating = 3 THEN 1 ELSE 0 END) as rating_3,
        SUM(CASE WHEN rating = 4 THEN 1 ELSE 0 END) as rating_4,
        SUM(CASE WHEN rating = 5 THEN 1 ELSE 0 END) as rating_5
      FROM reviews 
      WHERE product_id = ? AND status = 'approved'
    `, [productId]);
    
    // Format stats for frontend
    const formattedStats = {
      averageRating: stats[0].average_rating,
      totalReviews: stats[0].total_reviews,
      ratingCounts: {
        1: stats[0].rating_1,
        2: stats[0].rating_2,
        3: stats[0].rating_3,
        4: stats[0].rating_4,
        5: stats[0].rating_5
      }
    };
    
    // Get total count for pagination
    const [{ total }] = await db.query(
      'SELECT COUNT(*) as total FROM reviews WHERE product_id = ? AND status = "approved"',
      [productId]
    );
    
    res.json({
      reviews,
      stats: formattedStats,
      pagination: {
        total,
        pages: Math.ceil(total / limit),
        currentPage: parseInt(page),
        limit: parseInt(limit)
      }
    });
  } catch (error) {
    console.error('Error fetching reviews:', error);
    res.status(500).json({ error: 'Failed to fetch reviews' });
  }
});

// Mark a review as helpful
router.post('/reviews/:id/helpful', isAuthenticated, async (req, res) => {
  try {
    const reviewId = req.params.id;
    const userId = req.user.id;
    
    // Check if user already marked this review as helpful
    const existing = await db.query(
      'SELECT 1 FROM helpful_votes WHERE review_id = ? AND user_id = ?',
      [reviewId, userId]
    );
    
    if (existing.length > 0) {
      return res.status(400).json({ error: 'You have already marked this review as helpful' });
    }
    
    // Record the helpful vote
    await db.query(
      'INSERT INTO helpful_votes (review_id, user_id) VALUES (?, ?)',
      [reviewId, userId]
    );
    
    // Update the helpful count
    await db.query(
      'UPDATE reviews SET helpful_votes = helpful_votes + 1 WHERE id = ?',
      [reviewId]
    );
    
    res.json({ success: true });
  } catch (error) {
    console.error('Error marking review as helpful:', error);
    res.status(500).json({ error: 'Failed to mark review as helpful' });
  }
});

// Admin route to moderate reviews
router.put('/admin/reviews/:id', 
  [isAuthenticated, canModerateReviews],
  async (req, res) => {
    try {
      const { id } = req.params;
      const { status, moderationNotes } = req.body;
      
      if (!['approved', 'rejected'].includes(status)) {
        return res.status(400).json({ error: 'Invalid status' });
      }
      
      await db.query(
        'UPDATE reviews SET status = ?, moderation_notes = ? WHERE id = ?',
        [status, moderationNotes || null, id]
      );
      
      // If approving, update product rating averages
      if (status === 'approved') {
        const [review] = await db.query('SELECT product_id FROM reviews WHERE id = ?', [id]);
        
        if (review) {
          await db.query(`
            UPDATE products p 
            SET 
              p.avg_rating = (
                SELECT AVG(rating) FROM reviews 
                WHERE product_id = ? AND status = 'approved'
              ),
              p.review_count = (
                SELECT COUNT(*) FROM reviews 
                WHERE product_id = ? AND status = 'approved'
              )
            WHERE p.id = ?
          `, [review.product_id, review.product_id, review.product_id]);
        }
      }
      
      res.json({ success: true });
    } catch (error) {
      console.error('Error moderating review:', error);
      res.status(500).json({ error: 'Failed to moderate review' });
    }
  }
);

module.exports = router;

 

Advanced Features & Optimization

 

Review Moderation Strategies

 

  • Automatic Moderation: Use content moderation APIs like Amazon Comprehend, Google Cloud Natural Language, or OpenAI's Content Filter to detect problematic reviews
  • Review Workflow: Set up rules for auto-approval (verified purchases) vs. manual review
  • Admin Dashboard: Create a queue system for pending reviews that require moderator attention

 

// Example moderation service using external API
const axios = require('axios');

class ContentModeration {
  async checkForSpam(text) {
    try {
      // Using a hypothetical spam detection API
      const response = await axios.post('https://api.moderationservice.com/spam-detection', {
        text,
        threshold: 0.7 // Configurable threshold
      });
      
      return response.data.isSpam;
    } catch (error) {
      console.error('Spam detection error:', error);
      // Fail open: if the service is down, let reviews through rather than blocking them
      return false;
    }
  }
  
  filterProfanity(text) {
    // Simple word-replacement approach (production systems would use more sophisticated methods)
    const profanityList = ['badword1', 'badword2', 'badword3'];
    let filteredText = text;
    
    profanityList.forEach(word => {
      const regex = new RegExp(`\\b${word}\\b`, 'gi');
      filteredText = filteredText.replace(regex, '*'.repeat(word.length));
    });
    
    return filteredText;
  }
  
  async moderateMedia(imageUrl) {
    try {
      // Call a vision API to detect inappropriate content
      const response = await axios.post('https://api.moderationservice.com/image-moderation', {
        imageUrl
      });
      
      return {
        isAppropriate: response.data.isAppropriate,
        confidenceScore: response.data.confidenceScore,
        categories: response.data.categories // e.g., ["adult", "violence", etc.]
      };
    } catch (error) {
      console.error('Image moderation error:', error);
      // Fail closed: if service is down, reject the image
      return { isAppropriate: false };
    }
  }
}

module.exports = new ContentModeration();

 

Performance Optimization

 

  • Caching Strategy: Cache aggregated ratings and review statistics at the product level
  • Lazy Loading: Implement pagination and lazy loading for reviews to improve page load time
  • Database Indexes: Create the right indexes to speed up common queries

 

-- Essential indexes for review system performance
ALTER TABLE reviews ADD INDEX idx_product_status (product_id, status);
ALTER TABLE reviews ADD INDEX idx_user_product (user_id, product_id);
ALTER TABLE reviews ADD INDEX idx_created_at (created_at);
ALTER TABLE reviews ADD INDEX idx_rating (rating);
ALTER TABLE reviews ADD INDEX idx_helpful (helpful_votes);

 

Rich Review Features

 

// Example component for review analytics dashboard (React)
function ReviewAnalyticsDashboard({ productId }) {
  const [analytics, setAnalytics] = useState({
    ratingTrend: [],
    sentimentBreakdown: {},
    commonKeywords: [],
    responseRate: 0
  });
  
  useEffect(() => {
    async function fetchAnalytics() {
      const response = await fetch(`/api/analytics/reviews?productId=${productId}`);
      const data = await response.json();
      setAnalytics(data);
    }
    
    fetchAnalytics();
  }, [productId]);
  
  return (
    <div className="analytics-dashboard">
      <div className="card rating-trend">
        <h3>Rating Trend</h3>
        <LineChart 
          data={analytics.ratingTrend.map(point => ({
            date: new Date(point.date).toLocaleDateString(),
            avgRating: point.avgRating
          }))}
          xKey="date"
          yKey="avgRating"
          yDomain={[0, 5]}
        />
      </div>
      
      <div className="card sentiment-breakdown">
        <h3>Review Sentiment</h3>
        <PieChart 
          data={[
            { name: 'Positive', value: analytics.sentimentBreakdown.positive || 0 },
            { name: 'Neutral', value: analytics.sentimentBreakdown.neutral || 0 },
            { name: 'Negative', value: analytics.sentimentBreakdown.negative || 0 }
          ]}
        />
      </div>
      
      <div className="card keywords">
        <h3>Common Themes</h3>
        <ul className="tag-cloud">
          {analytics.commonKeywords.map(keyword => (
            <li 
              key={keyword.term} 
              style={{ fontSize: `${Math.max(1, Math.min(3, keyword.frequency / 5))}em` }}
            >
              {keyword.term}
            </li>
          ))}
        </ul>
      </div>
      
      <div className="card response-metrics">
        <h3>Owner Response Rate</h3>
        <div className="metric">
          <div className="metric-value">{(analytics.responseRate * 100).toFixed(1)}%</div>
          <div className="metric-label">of reviews received a response</div>
        </div>
        <div className="response-time">
          <div className="metric-value">{analytics.avgResponseTime || 'N/A'}</div>
          <div className="metric-label">Average response time</div>
        </div>
      </div>
    </div>
  );
}

 

Integration with Business Workflows

 

Automating Post-Purchase Review Solicitation

 

// Review solicitation service
class ReviewSolicitation {
  async scheduleEmailForOrder(order) {
    const { id: orderId, user_id: userId, items } = order;
    
    // Wait 14 days after purchase to ask for review
    const scheduledDate = new Date();
    scheduledDate.setDate(scheduledDate.getDate() + 14);
    
    // Prepare product data for the email
    const productDetails = await Promise.all(items.map(async (item) => {
      const [product] = await db.query('SELECT id, name, image_url FROM products WHERE id = ?', [item.product_id]);
      return {
        productId: product.id,
        name: product.name,
        image: product.image_url
      };
    }));
    
    // Get user email
    const [user] = await db.query('SELECT email, name FROM users WHERE id = ?', [userId]);
    
    // Check if user has already reviewed any of these products
    const existingReviews = await db.query(
      'SELECT product_id FROM reviews WHERE user_id = ? AND product_id IN (?)',
      [userId, items.map(item => item.product_id)]
    );
    
    const reviewedProductIds = existingReviews.map(review => review.product_id);
    
    // Filter out products that already have reviews
    const productsToReview = productDetails.filter(product => 
      !reviewedProductIds.includes(product.productId)
    );
    
    if (productsToReview.length === 0) {
      return; // No products to review
    }
    
    // Generate a signed token for the review page
    const token = this.generateReviewToken(userId, orderId, productsToReview.map(p => p.productId));
    
    // Schedule the email
    await db.query(
      'INSERT INTO scheduled_emails (user_id, email_type, scheduled_date, template_data, status) VALUES (?, ?, ?, ?, ?)',
      [
        userId,
        'review_request',
        scheduledDate,
        JSON.stringify({
          userName: user.name,
          userEmail: user.email,
          products: productsToReview,
          reviewUrl: `https://yoursite.com/review?token=${token}`
        }),
        'scheduled'
      ]
    );
  }
  
  generateReviewToken(userId, orderId, productIds) {
    // In a real implementation, use a secure method like JWT
    // to generate a time-limited token that verifies the user
    // and links to specific products
    const jwt = require('jsonwebtoken');
    
    return jwt.sign(
      { userId, orderId, productIds },
      process.env.JWT_SECRET,
      { expiresIn: '30d' } // Token expires in 30 days
    );
  }
}

module.exports = new ReviewSolicitation();

 

Review Response System for Business Owners

 

// Admin-side component for responding to reviews
function ReviewResponseComponent({ review, onRespond }) {
  const [responseText, setResponseText] = useState(review.business_response || '');
  const [isSubmitting, setIsSubmitting] = useState(false);
  
  const handleSubmit = async (e) => {
    e.preventDefault();
    setIsSubmitting(true);
    
    try {
      const response = await fetch(`/api/admin/reviews/${review.id}/respond`, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ responseText })
      });
      
      if (!response.ok) throw new Error('Failed to submit response');
      
      onRespond({ ...review, business_response: responseText });
      toast.success('Your response has been published');
    } catch (error) {
      console.error('Error responding to review:', error);
      toast.error('Failed to publish your response');
    } finally {
      setIsSubmitting(false);
    }
  };
  
  return (
    <form onSubmit={handleSubmit} className="review-response-form">
      <div className="review-content">
        <div className="review-details">
          <div className="stars">
            {[1, 2, 3, 4, 5].map(star => (
              <span key={star} className={review.rating >= star ? 'star filled' : 'star'}>★</span>
            ))}
          </div>
          <div className="review-date">{new Date(review.created_at).toLocaleDateString()}</div>
        </div>
        
        <div className="review-text">{review.review_text}</div>
      </div>
      
      <div className="response-editor">
        <h4>Your Response</h4>
        <textarea
          value={responseText}
          onChange={(e) => setResponseText(e.target.value)}
          placeholder="Thank the reviewer for their feedback..."
          rows={4}
        />
        
        <div className="response-tips">
          <h5>Response Tips</h5>
          <ul>
            <li>Address the reviewer by name</li>
            <li>Thank them for their feedback</li>
            <li>Address specific points they mentioned</li>
            <li>For negative reviews, explain how you're addressing their concerns</li>
            <li>Keep it professional, even for critical reviews</li>
          </ul>
        </div>
        
        <div className="button-group">
          <button type="submit" disabled={isSubmitting || !responseText.trim()}>
            {isSubmitting ? 'Publishing...' : 'Publish Response'}
          </button>
        </div>
      </div>
    </form>
  );
}

 

Testing Your Review System

 

Key Test Scenarios

 

// Jest test examples for review functionality
describe('Review System', () => {
  describe('Review Submission', () => {
    test('should not allow users to review products they have not purchased', async () => {
      // Mock user that hasn't purchased the product
      const mockUser = { id: 123, name: 'Test User' };
      
      const response = await request(app)
        .post('/api/reviews')
        .set('Authorization', `Bearer ${generateTokenForUser(mockUser)}`)
        .send({
          productId: 456,
          rating: 5,
          reviewText: 'This is a great product, totally not fake!'
        });
      
      expect(response.status).toBe(400);
      expect(response.body).toHaveProperty('error');
      expect(response.body.error).toContain('purchase');
    });
    
    test('should detect and reject spam reviews', async () => {
      // Mock verified purchaser
      const mockUser = { id: 123, name: 'Test User' };
      
      // Mock spam text (repeated keywords, excessive URLs, etc.)
      const spamText = 'BUY NOW! GREAT DEAL! CLICK HERE: http://spam.com CLICK HERE: http://spam2.com AMAZING!';
      
      const response = await request(app)
        .post('/api/reviews')
        .set('Authorization', `Bearer ${generateTokenForUser(mockUser)}`)
        .send({
          productId: 456,
          rating: 5,
          reviewText: spamText
        });
      
      expect(response.status).toBe(400);
      expect(response.body).toHaveProperty('error');
      expect(response.body.error).toContain('spam');
    });
    
    test('should filter profanity but still accept the review', async () => {
      // Mock verified purchaser
      const mockUser = { id: 123, name: 'Test User' };
      
      const reviewWithProfanity = 'This product is badword1 amazing!';
      
      const response = await request(app)
        .post('/api/reviews')
        .set('Authorization', `Bearer ${generateTokenForUser(mockUser)}`)
        .send({
          productId: 456,
          rating: 4,
          reviewText: reviewWithProfanity
        });
      
      expect(response.status).toBe(201);
      
      // Check if the stored review has filtered content
      const storedReview = await db.query(
        'SELECT review_text FROM reviews WHERE user_id = ? AND product_id = ?',
        [mockUser.id, 456]
      );
      
      expect(storedReview[0].review_text).toBe('This product is ******* amazing!');
    });
  });
  
  describe('Review Aggregation', () => {
    test('should correctly calculate average rating after new review', async () => {
      // Setup: Add some reviews
      await db.query('INSERT INTO reviews (user_id, product_id, rating, status) VALUES (1, 789, 4, "approved")');
      await db.query('INSERT INTO reviews (user_id, product_id, rating, status) VALUES (2, 789, 2, "approved")');
      
      // Submit a new 5-star review
      await request(app)
        .post('/api/reviews')
        .set('Authorization', `Bearer ${generateTokenForUser({ id: 3 })}`)
        .send({
          productId: 789,
          rating: 5,
          reviewText: 'Excellent product!'
        });
      
      // Check the product's updated average rating (should be (4+2+5)/3 = 3.67)
      const [product] = await db.query('SELECT avg_rating FROM products WHERE id = ?', [789]);
      expect(parseFloat(product.avg_rating)).toBeCloseTo(3.67, 2);
    });
  });
  
  describe('Review Helpfulness', () => {
    test('should not allow a user to mark a review as helpful multiple times', async () => {
      // Setup: Create a review
      const [result] = await db.query(
        'INSERT INTO reviews (user_id, product_id, rating, review_text, status) VALUES (?, ?, ?, ?, ?) RETURNING id',
        [1, 123, 4, 'Good product', 'approved']
      );
      
      const reviewId = result.id;
      
      // First helpful vote should succeed
      const firstResponse = await request(app)
        .post(`/api/reviews/${reviewId}/helpful`)
        .set('Authorization', `Bearer ${generateTokenForUser({ id: 2 })}`);
      
      expect(firstResponse.status).toBe(200);
      
      // Second attempt should fail
      const secondResponse = await request(app)
        .post(`/api/reviews/${reviewId}/helpful`)
        .set('Authorization', `Bearer ${generateTokenForUser({ id: 2 })}`);
      
      expect(secondResponse.status).toBe(400);
      expect(secondResponse.body).toHaveProperty('error');
      expect(secondResponse.body.error).toContain('already marked');
    });
  });
});

 

Implementation Strategy

 

Phased Rollout Approach

 

  1. Phase 1: Core Review System
    • Basic review submission form with star ratings and text
    • Simple display of reviews with sorting and pagination
    • Database schema and API endpoints
  2. Phase 2: Enhanced Features
    • Media uploads for reviews
    • Helpful voting system
    • Verified purchase badges
    • Basic moderation system
  3. Phase 3: Business Integration
    • Review solicitation emails
    • Owner response capability
    • Review analytics dashboard
    • Advanced moderation with AI/ML

 

Conclusion

 

Implementing a review system is a balance of technical architecture and business strategy. The best systems evolve with your needs—starting with a solid foundation and expanding thoughtfully. By focusing on the right database design, clean API architecture, and progressive enhancement, you can build a review system that not only provides social proof for your products but becomes a valuable source of customer intelligence.

Remember that reviews aren't just data—they're conversations with your customers. Design your system to respect the customer's voice while protecting your platform from abuse. The effort invested in a well-designed review system typically returns multiples in customer trust, engagement, and valuable product feedback.

Ship User Reviews & Ratings 10x Faster with RapidDev

Connect with our team to unlock the full potential of code solutions with a no-commitment consultation!

Book a Free Consultation

Top 3 User Reviews & Ratings Usecases

Explore the top 3 ways user reviews and ratings boost engagement and trust in your web app.

 

Social Proof & Trust Building

 

  • Leverages psychological principles of social validation to overcome purchase hesitation - customers are 63% more likely to purchase from a site with reviews than one without them.
  •  

  • Serves as a trust-building mechanism that effectively outsources credibility to your existing customer base, creating a self-reinforcing cycle of trust that's far more powerful than your own marketing claims.
  •  

  • Functions as constantly refreshing content that improves SEO performance while simultaneously reducing the perceived risk for new customers - particularly valuable for high-consideration products or services.

 

 

Product Development Intelligence

 

  • Creates a continuous feedback loop that highlights product shortcomings and unexpected use cases, effectively turning your customer base into an extension of your product research team.
  •  

  • Provides granular, contextual insights about how different customer segments interact with your product - revealing patterns that quantitative analytics alone would miss.
  •  

  • Reduces support costs by surfacing common issues quickly, allowing you to address problems either through product improvements or preemptive documentation before they generate support tickets.

 

 

Customer Engagement & Retention

 

  • Creates a bi-directional relationship with customers who leave reviews, increasing their psychological investment in your brand - customers who write reviews have a 105% higher conversion rate on subsequent purchases.
  •  

  • Establishes a community dynamic around your product when you respond to reviews, transforming a transactional relationship into an ongoing conversation that increases lifetime value.
  •  

  • Provides opportunities for service recovery by publicly addressing negative feedback, which can turn detractors into advocates - 45% of consumers say they're more likely to visit businesses that respond to negative reviews.

 


Recognized by the best

Trusted by 600+ businesses globally

From startups to enterprises and everything in between, see for yourself our incredible impact.

RapidDev was an exceptional project management organization and the best development collaborators I've had the pleasure of working with.

They do complex work on extremely fast timelines and effectively manage the testing and pre-launch process to deliver the best possible product. I'm extremely impressed with their execution ability.

Arkady
CPO, Praction
Working with Matt was comparable to having another co-founder on the team, but without the commitment or cost.

He has a strategic mindset and willing to change the scope of the project in real time based on the needs of the client. A true strategic thought partner!

Donald Muir
Co-Founder, Arc
RapidDev are 10/10, excellent communicators - the best I've ever encountered in the tech dev space.

They always go the extra mile, they genuinely care, they respond quickly, they're flexible, adaptable and their enthusiasm is amazing.

Mat Westergreen-Thorne
Co-CEO, Grantify
RapidDev is an excellent developer for custom-code solutions.

We’ve had great success since launching the platform in November 2023. In a few months, we’ve gained over 1,000 new active users. We’ve also secured several dozen bookings on the platform and seen about 70% new user month-over-month growth since the launch.

Emmanuel Brown
Co-Founder, Church Real Estate Marketplace
Matt’s dedication to executing our vision and his commitment to the project deadline were impressive. 

This was such a specific project, and Matt really delivered. We worked with a really fast turnaround, and he always delivered. The site was a perfect prop for us!

Samantha Fekete
Production Manager, Media Production Company
The pSEO strategy executed by RapidDev is clearly driving meaningful results.

Working with RapidDev has delivered measurable, year-over-year growth. Comparing the same period, clicks increased by 129%, impressions grew by 196%, and average position improved by 14.6%. Most importantly, qualified contact form submissions rose 350%, excluding spam.

Appreciation as well to Matt Graham for championing the collaboration!

Michael W. Hammond
Principal Owner, OCD Tech

We put the rapid in RapidDev

Need a dedicated strategic tech and growth partner? Discover what RapidDev can do for your business! Book a call with our team to schedule a free, no-obligation consultation. We’ll discuss your project and provide a custom quote at no cost.Â