Tours
Introduction
One of my most rewarding projects is Tours, a tourism platform where users can explore, book, and review tours, while administrators and guides manage their side of the operations.
In this article, I’ll dive into the technical details — the architecture, features, and techniques — that I used to build a robust and scalable platform for both travelers and agency.
Features of the Tours Backend
I built Tours using the MEN stack (MongoDB, Express, Node.js) with a focus on Node.js for the backend. The project follows the MVC (Model-View-Controller) architecture and uses Pug templates for the view layer.
Below are the core functionalities implemented:
- User Management:
- Sign-up/Login: Users can create accounts and log in using secure authentication with JWT tokens.
- Password Management: Users can change passwords or reset forgotten passwords.
- Nodemailer Integration: Automated emails for actions like account registration and password reset.
2. Admin and Guide Functionalities
Get Priyanshu Bisht’s stories in your inbox
Join Medium for free to get updates from this writer.
Admin Tasks:
- Post tours with geographic coordinates for mapping.
- Moderate reviews, including the ability to delete user reviews.
Guide Tasks:
- Assist users during the tours (scope can expand based on future requirements).
3. Tour Booking & Payments
- Stripe Integration: Users can book tours and make secure payments through Stripe.
4. Review System
- Users can post and manage their reviews.
- Admins can delete inappropriate reviews.
Database Design and Referencing
MongoDB serves as the database, with well-defined referencing to manage relations between entities. For example:
- Users reference their bookings and reviews.
- Tours reference guides, locations, and associated reviews.
This approach ensures a normalized structure, making data retrieval efficient.
Key Data Pipelines in the Tourism Application Backend
- User Registration and Authentication:
userSchema.pre('save', async function (next) {
if (!this.isModified('password')) return next();
// Hash the password before storing it in the DB.
this.password = await bcrypt.hash(this.password, 12);
// Remove the passwordConfirm field (not needed in the database)
this.passwordConfirm = undefined;
next();
});
userSchema.pre('save', function (next) {
// Skip this if the password hasn't been modified or this is a new document
if (!this.isModified('password') || this.isNew) return next();
// Set passwordChangedAt to slightly earlier than the current time.
// This ensures JWT is created after the password change is reflected
this.passwordChangedAt = Date.now() - 1000;
next();
});
userSchema.pre(/^find/, function (next) {
// Add a condition to exclude users with `active` set to false
this.find({ active: { $ne: false } });
next();
});
userSchema.methods.correctPassword = async function (
candidatePassword,
userPassword
) {
// Use bcryptjs to compare the candidate password with the stored hashed password
return await bcrypt.compare(candidatePassword, userPassword);
};
userSchema.methods.changedPasswordAfter = function (JWTTimestamp) {
if (this.passwordChangedAt) {
const changedTimestamp = parseInt(
this.passwordChangedAt.getTime() / 1000,
10
);
// If the JWT was issued before the password change, return true
return JWTTimestamp < changedTimestamp;
}
// Otherwise, return false (no password change after JWT issue)
return false;
};
// Instance Method: Create a password reset token
userSchema.methods.createPasswordResetToken = function () {
const resetToken = crypto.randomBytes(32).toString('hex');
this.passwordResetToken = crypto
.createHash('sha256')
.update(resetToken)
.digest('hex');
this.passwordResetExpires = Date.now() + 10 * 60 * 1000;
return resetToken;
};2. Aggregation Pipeline implementation:
// Ensure that a user can only leave one review for a particular tour
reviewSchema.index({ user: 1, tour: 1 }, { unique: true });
// This improves query performance and avoids repetitive population logic in each query
reviewSchema.pre(/^find/, function (next) {
this.populate({
path: 'user', // Populate the 'user' field
select: 'name photo', // Select only 'name' and 'photo' fields for user
});
next();
});
reviewSchema.statics.calcAverageRatings = async function (tourId) {
// Use aggregation to calculate total ratings and average rating for the specified tour
const stats = await this.aggregate([
{
$match: { tour: tourId }, // Match reviews for the given tourId
},
{
$group: {
_id: '$tour', // Group by the tourId
nRating: { $sum: 1 }, // Count the number of reviews (ratings)
avgRating: { $avg: '$rating' }, // Calculate the average rating
},
},
]);
// If there are any reviews for the tour, update the tour's ratings data
if (stats.length > 0) {
await Tour.findByIdAndUpdate(tourId, {
ratingsQuantity: stats[0].nRating, // Update the total number of ratings
ratingsAverage: stats[0].avgRating, // Update the average rating
});
} else {
// If no reviews exist for the tour, set default values for ratings
await Tour.findByIdAndUpdate(tourId, {
ratingsQuantity: 0,
ratingsAverage: 4.5, // Set a default average rating if no reviews exist
});
}
console.log(stats); // Optionally log the stats to verify the aggregation result
};
reviewSchema.post('save', function () {
this.constructor.calcAverageRatings(this.tour);
// Call the calcAverageRatings method
});
reviewSchema.pre(/^findOneAnd/, async function (next) {
this.r = await this.findOne(); // Find the review document before proceeding
next(); // Proceed with the next middleware or operation
});
reviewSchema.post(/^findOneAnd/, async function (next) {
await this.r.constructor.calcAverageRatings(this.r.tour);
// Recalculate the average ratings for the tour
});API Endpoints
Checkout the entire API Documentation:
Challenges and Learnings
- Scalable Architecture: The MVC pattern streamlined development and allowed for easy debugging and maintenance. By separating concerns, it made my codebase more organized and easier to scale as features were added.
- Email Templates: Crafting email templates that worked seamlessly across devices involved several iterations. The main challenge was ensuring that the templates were user-friendly and responsive.
- Data Pipelines: One of the key learnings from this project was the implementation of data pipelines within the models. These pipelines helped streamline the data processing and validation before it was passed to the controller. For example, in the Tour model, the slug generation was automated through a pipeline, ensuring a consistent URL structure without redundant data handling. This not only made the backend more efficient but also significantly reduced errors related to data integrity. Data pipelines also allowed for the pre-processing of data (such as ensuring the duration and price of tours were always validated correctly), and improved the readability of code by making model operations cleaner and more modular.
This experience deepened my understanding of data flow within the system and improved the maintainability of the backend.
Conclusion
The Tours backend is a robust and scalable system for tourism platforms. From user authentication to tour bookings, it covers the essentials and micro-functionalities, ensuring a seamless user experience. This project significantly deepened my understanding of backend development and scalable architecture.
Have some feedback? Drop them in the comments below!








