Sitemap

Tours

5 min readDec 16, 2024

--

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.

Press enter or click to view image in full size
Application Flowchart

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:

  1. 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.
Press enter or click to view image in full size
Sign in Welcome!
Sign in Welcome
Press enter or click to view image in full size
Password reset token
The reset token is sent to the client via the email

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.

Press enter or click to view image in full size
ER diagram for the app

Key Data Pipelines in the Tourism Application Backend

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

Press enter or click to view image in full size
All the API Endpoints

Checkout the entire API Documentation:

Challenges and Learnings

  1. 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.
  2. 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.
  3. 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!

--

--

Priyanshu Bisht
Priyanshu Bisht

Aspiring Software Engineer | Backend (Node.js, Express.js, MongoDB, Redis) | Docker | AWS | C++ | Problem Solving