Node.js and Express.js part 9

Let’s dive into reviewing our bootcamps

Prakash Jha
6 min readOct 6, 2020

let’s first get started by getting all the reviews and reviews by bootcamps

create models/Review.js

const mongoose = require('mongoose');const ReviewSchema = new mongoose.Schema({
title: {
type: String,
trim: true,
required: [true, 'Please add a title for the review'],
maxlength: 100
},
text: {
type: String,
required: [true, 'Please add some text']
},
rating: {
type: Number,
min: 1,
max: 10,
required: [true, 'Please add a rating between 1 and 10']
},
createdAt: {
type: Date,
default: Date.now
},
bootcamp: {
type: mongoose.Schema.ObjectId,
ref: 'Bootcamp',
required: true
},
user: {
type: mongoose.Schema.ObjectId,
ref: 'User',
required: true
}
});
module.exports = mongoose.model('Review', ReviewSchema);

and controllers/reviews.js

const ErrorResponse = require('../utils/errorResponse');
const asyncHandler = require('../middleware/async');
const Review = require('../models/Review');
const Bootcamp = require('../models/Bootcamp');
// @desc Get reviews
// @route GET /api/v1/reviews
// @route GET /api/v1/bootcamps/:bootcampId/reviews
// @access Public
exports.getReviews = asyncHandler(async (req, res, next) => {
if (req.params.bootcampId) {
const reviews = await Review.find({ bootcamp: req.params.bootcampId });
return res.status(200).json({
success: true,
count: reviews.length,
data: reviews
});
} else {
res.status(200).json(res.advancedResults);
}
});

and in routes/reviews.js

const express = require('express');
const {
getReviews
} = require('../controllers/reviews');
const Review = require('../models/Review');const router = express.Router({ mergeParams: true });const advancedResults = require('../middleware/advancedResults');
const { protect, authorize } = require('../middleware/auth');
router
.route('/')
.get(
advancedResults(Review, {
path: 'bootcamp',
select: 'name description'
}),
getReviews
);
module.exports = router;

and update the whole routes/bootcamps.js with this

const express = require('express');
const {
getBootcamps,
getBootcamp,
createBootcamp,
updateBootcamp,
deleteBootcamp,
getBootcampsInRadius,
bootcampPhotoUpload,
} = require('../controllers/bootcamps');
const Bootcamp = require('../models/Bootcamp');
const advancedResults = require('../middleware/advancedResults');
//include other resourse router
const courseRouter = require('./courses');
const reviewRouter = require('./reviews');
const router = express.Router();const { protect,authorize } = require('../middleware/auth');router.use('/:bootcampId/courses', courseRouter);
router.use('/:bootcampId/reviews', reviewRouter);
router.route('/radius/:zipcode/:distance').get(getBootcampsInRadius);router
.route('/')
.get(advancedResults(Bootcamp, 'courses'), getBootcamps)
.post(protect,authorize('publisher','admin'), createBootcamp);
router.route('/:id/photo').put(protect, authorize('publisher','admin'),bootcampPhotoUpload);router
.route('/:id')
.get(getBootcamp)
.put(protect, authorize('publisher','admin'),updateBootcamp)
.delete(protect, authorize('publisher','admin'),deleteBootcamp);
module.exports = router;

and include the routes in server.js

get single review and update seeder

include this as reviews.json in seeder.js for seeding into database

[
{
"_id": "5d7a514b5d2c12c7449be020",
"title": "Learned a ton!",
"text": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec viverra feugiat mauris id viverra. Duis luctus ex sed facilisis ultrices. Curabitur scelerisque bibendum ligula, quis condimentum libero fermentum in. Aenean erat erat, aliquam in purus a, rhoncus hendrerit tellus. Donec accumsan justo in felis consequat sollicitudin. Fusce luctus mattis nunc vitae maximus. Curabitur semper felis eu magna laoreet scelerisque",
"rating": "8",
"bootcamp": "5d713995b721c3bb38c1f5d0",
"user": "5c8a1d5b0190b214360dc033"
},
{
"_id": "5d7a514b5d2c12c7449be021",
"title": "Great bootcamp",
"text": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec viverra feugiat mauris id viverra. Duis luctus ex sed facilisis ultrices. Curabitur scelerisque bibendum ligula, quis condimentum libero fermentum in. Aenean erat erat, aliquam in purus a, rhoncus hendrerit tellus. Donec accumsan justo in felis consequat sollicitudin. Fusce luctus mattis nunc vitae maximus. Curabitur semper felis eu magna laoreet scelerisque",
"rating": "10",
"bootcamp": "5d713995b721c3bb38c1f5d0",
"user": "5c8a1d5b0190b214360dc034"
},
{
"_id": "5d7a514b5d2c12c7449be022",
"title": "Got me a developer job",
"text": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec viverra feugiat mauris id viverra. Duis luctus ex sed facilisis ultrices. Curabitur scelerisque bibendum ligula, quis condimentum libero fermentum in. Aenean erat erat, aliquam in purus a, rhoncus hendrerit tellus. Donec accumsan justo in felis consequat sollicitudin. Fusce luctus mattis nunc vitae maximus. Curabitur semper felis eu magna laoreet scelerisque",
"rating": "7",
"bootcamp": "5d713a66ec8f2b88b8f830b8",
"user": "5c8a1d5b0190b214360dc035"
},
{
"_id": "5d7a514b5d2c12c7449be023",
"title": "Not that great",
"text": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec viverra feugiat mauris id viverra. Duis luctus ex sed facilisis ultrices. Curabitur scelerisque bibendum ligula, quis condimentum libero fermentum in. Aenean erat erat, aliquam in purus a, rhoncus hendrerit tellus. Donec accumsan justo in felis consequat sollicitudin. Fusce luctus mattis nunc vitae maximus. Curabitur semper felis eu magna laoreet scelerisque",
"rating": "4",
"bootcamp": "5d713a66ec8f2b88b8f830b8",
"user": "5c8a1d5b0190b214360dc036"
},
{
"_id": "5d7a514b5d2c12c7449be024",
"title": "Great overall experience",
"text": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec viverra feugiat mauris id viverra. Duis luctus ex sed facilisis ultrices. Curabitur scelerisque bibendum ligula, quis condimentum libero fermentum in. Aenean erat erat, aliquam in purus a, rhoncus hendrerit tellus. Donec accumsan justo in felis consequat sollicitudin. Fusce luctus mattis nunc vitae maximus. Curabitur semper felis eu magna laoreet scelerisque",
"rating": "7",
"bootcamp": "5d725a037b292f5f8ceff787",
"user": "5c8a1d5b0190b214360dc037"
},
{
"_id": "5d7a514b5d2c12c7449be025",
"title": "Not worth the money",
"text": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec viverra feugiat mauris id viverra. Duis luctus ex sed facilisis ultrices. Curabitur scelerisque bibendum ligula, quis condimentum libero fermentum in. Aenean erat erat, aliquam in purus a, rhoncus hendrerit tellus. Donec accumsan justo in felis consequat sollicitudin. Fusce luctus mattis nunc vitae maximus. Curabitur semper felis eu magna laoreet scelerisque",
"rating": "5",
"bootcamp": "5d725a037b292f5f8ceff787",
"user": "5c8a1d5b0190b214360dc038"
},
{
"_id": "5d7a514b5d2c12c7449be026",
"title": "Best instructors",
"text": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec viverra feugiat mauris id viverra. Duis luctus ex sed facilisis ultrices. Curabitur scelerisque bibendum ligula, quis condimentum libero fermentum in. Aenean erat erat, aliquam in purus a, rhoncus hendrerit tellus. Donec accumsan justo in felis consequat sollicitudin. Fusce luctus mattis nunc vitae maximus. Curabitur semper felis eu magna laoreet scelerisque",
"rating": "10",
"bootcamp": "5d725a1b7b292f5f8ceff788",
"user": "5c8a1d5b0190b214360dc039"
},
{
"_id": "5d7a514b5d2c12c7449be027",
"title": "Was worth the investment",
"text": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec viverra feugiat mauris id viverra. Duis luctus ex sed facilisis ultrices. Curabitur scelerisque bibendum ligula, quis condimentum libero fermentum in. Aenean erat erat, aliquam in purus a, rhoncus hendrerit tellus. Donec accumsan justo in felis consequat sollicitudin. Fusce luctus mattis nunc vitae maximus. Curabitur semper felis eu magna laoreet scelerisque",
"rating": "7",
"bootcamp": "5d725a1b7b292f5f8ceff788",
"user": "5c8a1d5b0190b214360dc040"
}
]

add this peice of code in controllers/reviews.js

// @desc      Get single review
// @route GET /api/v1/reviews/:id
// @access Public
exports.getReview = asyncHandler(async (req, res, next) => {
const review = await Review.findById(req.params.id).populate({
path: 'bootcamp',
select: 'name description'
});

if (!review) {
return next(
new ErrorResponse(`No review found with the id of ${req.params.id}`, 404)
);
}

res.status(200).json({
success: true,
data: review
});
});

and also include the routes in routes/reviews.js

router
.route('/:id')
.get(getReview);

now lets write a function to add a review

as only 1 review can be given by a user to any bootcamp so we add this in models/Review.js

// Prevent user from submitting more than one review per bootcamp
ReviewSchema.index({ bootcamp: 1, user: 1 }, { unique: true });

and add this piece of code in conrollers/reviews.js

// @desc      Add review
// @route POST /api/v1/bootcamps/:bootcampId/reviews
// @access Private
exports.addReview = asyncHandler(async (req, res, next) => {
req.body.bootcamp = req.params.bootcampId;
req.body.user = req.user.id;

const bootcamp = await Bootcamp.findById(req.params.bootcampId);

if (!bootcamp) {
return next(
new ErrorResponse(
`No bootcamp with the id of ${req.params.bootcampId}`,
404
)
);
}

const review = await Review.create(req.body);

res.status(201).json({
success: true,
data: review
});
});

and update the routes in routes/reviews.js

router
.route('/')
.get(
advancedResults(Review, {
path: 'bootcamp',
select: 'name description'
}),
getReviews
)
.post(protect, authorize('user', 'admin'), addReview);

now we need to calculate the average rating given to bootcamp

go to models/Review.js and add this code above module.exports = ….

// Call getAverageCost after save
ReviewSchema.post('save', async function() {
await this.constructor.getAverageRating(this.bootcamp);
});
// Call getAverageCost before remove
ReviewSchema.post('remove', async function() {
await this.constructor.getAverageRating(this.bootcamp);
});

now lets write the code to update and delete the reviews

go to controllers/reviews.js

// @desc      Update review
// @route PUT /api/v1/reviews/:id
// @access Private
exports.updateReview = asyncHandler(async (req, res, next) => {
let review = await Review.findById(req.params.id);

if (!review) {
return next(
new ErrorResponse(`No review with the id of ${req.params.id}`, 404)
);
}

// Make sure review belongs to user or user is admin
if (review.user.toString() !== req.user.id && req.user.role !== 'admin') {
return next(new ErrorResponse(`Not authorized to update review`, 401));
}

review = await Review.findByIdAndUpdate(req.params.id, req.body, {
new: true,
runValidators: true
});

res.status(200).json({
success: true,
data: review
});
});

// @desc Delete review
// @route DELETE /api/v1/reviews/:id
// @access Private
exports.deleteReview = asyncHandler(async (req, res, next) => {
const review = await Review.findById(req.params.id);

if (!review) {
return next(
new ErrorResponse(`No review with the id of ${req.params.id}`, 404)
);
}

// Make sure review belongs to user or user is admin
if (review.user.toString() !== req.user.id && req.user.role !== 'admin') {
return next(new ErrorResponse(`Not authorized to update review`, 401));
}

await review.remove();

res.status(200).json({
success: true,
data: {}
});
});

and update the whole code of routes/reviews.js with this

const express = require('express');
const {
getReviews,
getReview,
addReview,
updateReview,
deleteReview
} = require('../controllers/reviews');
const Review = require('../models/Review');const router = express.Router({ mergeParams: true });const advancedResults = require('../middleware/advancedResults');
const { protect, authorize } = require('../middleware/auth');
router
.route('/')
.get(
advancedResults(Review, {
path: 'bootcamp',
select: 'name description'
}),
getReviews
)
.post(protect, authorize('user', 'admin'), addReview);
router
.route('/:id')
.get(getReview)
.put(protect, authorize('user', 'admin'), updateReview)
.delete(protect, authorize('user', 'admin'), deleteReview);;
module.exports = router;

the format to make a post request is to go to routes like this

{{URL}}/api/v1/bootcamps/5f7cb8d814a9866e474b4940/reviews

and make post request with a format like this

{
"title":"great bcdootcamp",
"text":"i learnexcxcd a lot",
"rating":10
}

so yea that’s it we have made crud for reviews

--

--