CRUD and Aggregation

ReviewsService

src/reviews/reviews.service.ts

import { ConflictException, Injectable } from '@nestjs/common';
import { InjectModel } from '@nestjs/mongoose';
import { Model } from 'mongoose';
import { Review } from './schemas/review.schema';
import { CreateReviewDto } from './dto/create-review.dto';

@Injectable()
export class ReviewsService {
  constructor(@InjectModel(Review.name) private readonly reviewModel: Model<Review>) {}

  async create(userId: number, dto: CreateReviewDto) {
    try {
      return await this.reviewModel.create({ ...dto, userId });
    } catch (e) {
      if ((e as { code?: number }).code === 11000) throw new ConflictException('You already reviewed this product');
      throw e;
    }
  }

  findForProduct(productId: number, page = 1, limit = 10) {
    return this.reviewModel
      .find({ productId })
      .sort({ createdAt: -1 })
      .skip((page - 1) * limit)
      .limit(limit)
      .lean()                                  // plain objects, faster
      .exec();
  }

  async ratingSummary(productId: number) {
    const [summary] = await this.reviewModel.aggregate<{ avg: number; count: number }>([
      { $match: { productId } },
      { $group: { _id: '$productId', avg: { $avg: '$rating' }, count: { $sum: 1 } } },
      { $project: { _id: 0, avg: { $round: ['$avg', 1] }, count: 1 } },
    ]);
    return summary ?? { avg: 0, count: 0 };
  }
}

Requests

Example

curl -X POST http://localhost:3000/reviews -H "Authorization: Bearer <token>" -H "Content-Type: application/json" \
  -d '{"productId":7,"rating":4,"comment":"Good bass, battery lasts 2 days"}'
curl http://localhost:3000/products/7/rating

Example output

{"productId":7,"userId":12,"rating":4,"comment":"Good bass, battery lasts 2 days","images":[],"verifiedPurchase":false,"_id":"6787a1c0e4b0a5d1f2c3b4a9","createdAt":"2026-01-15T10:00:00.000Z","updatedAt":"2026-01-15T10:00:00.000Z","__v":0}
{"avg":4.3,"count":1287}

What the Aggregation Computes

Example

The $match / $group / $project pipeline above, done on an array so you can see each step:

type Review = { productId: number; rating: number };
const reviews: Review[] = [
  { productId: 7, rating: 5 }, { productId: 7, rating: 4 }, { productId: 8, rating: 2 },
  { productId: 7, rating: 4 }, { productId: 7, rating: 3 }, { productId: 8, rating: 5 },
];

function ratingSummary(productId: number) {
  const matched = reviews.filter((r) => r.productId === productId);           // $match
  const sum = matched.reduce((s, r) => s + r.rating, 0);                         // $group
  const count = matched.length;
  return { avg: count ? Math.round((sum / count) * 10) / 10 : 0, count };        // $project + $round
}

console.log('product 7:', ratingSummary(7));
console.log('product 8:', ratingSummary(8));
console.log('product 9:', ratingSummary(9));

Output

product 7: { avg: 4, count: 4 }
product 8: { avg: 3.5, count: 2 }
product 9: { avg: 0, count: 0 }