Building a Course-Selling Web Application with Node.js, Express, and MongoDB

Project Structure

The project consists of several key components:

  • User Router: Handles user registration, login, and fetching purchased courses.

  • Course Router: Manages course listings and purchases.

  • Models: Mongoose models for users, courses, and purchases.

  • Middlewares: For authentication and rate limiting.

Setting Up the Environment

Before we dive into the code, make sure to set up your environment:

  1. Node.js: Ensure Node.js is installed on your machine.

  2. MongoDB: Have a MongoDB database ready for storing user, course, and purchase information.

  3. Dependencies: Install necessary packages using npm:

     npm install express mongoose dotenv bcrypt jsonwebtoken zod express-rate-limit
    

Key Components

1. User Authentication

The user authentication system uses JWT (JSON Web Tokens) to manage user sessions. Here’s how the signup and signin processes work:

  • Signup: Users provide their email, username, password, and full name. The password is hashed using bcrypt before storing it in the database.

      userRouter.post('/signup', loginLimit, async (req, res) => {
          try {
              const signupObject = z.object({
                  email: z.string().email({ message: 'provide valid email' }),
                  username: z.string().min(3, { message: 'min 3 characters needed' }).max(15, { message: 'max 15 allowed' }),
                  password: z.string().min(6, { message: 'min 6 characters required' }).max(15, { message: 'max 15 is allowed' }),
                  fullName: z.string().min(3, { message: 'min 3 characters required' }).max(15, { message: 'max 15 is allowed' })
              });
    
              const parsedObject = signupObject.safeParse(req.body);
              if (!parsedObject.success) {
                  return res.status(400).json({
                      message: 'incorrect data',
                      error: parsedObject.error.errors
                  });
              }
    
              const { email, username, password, fullName } = parsedObject.data;
              const hashedPassword = await bcrypt.hash(password, 10);
    
              await userModel.create({
                  email,
                  username,
                  password: hashedPassword,
                  fullName
              });
    
              res.status(201).json({
                  message: 'user created successfully'
              });
          } catch (error) {
              res.status(500).json({
                  message: `unable to create user, error: ${error.message}`
              });
          }
      });
    
  • Signin: Users can log in using their email and password. If the credentials match, a token is generated and sent to the user.

      userRouter.post('/signin', loginLimit, async (req, res) => {
          try {
              const signinObject = z.object({
                  email: z.string().email({ message: 'provide a valid email' }),
                  password: z.string().min(6, { message: 'valid password required' })
              });
    
              const parsedObject = signinObject.safeParse(req.body);
              if (!parsedObject.success) {
                  return res.status(400).json({
                      message: 'invalid credentials',
                      error: parsedObject.error.errors
                  });
              }
    
              const { email, password } = parsedObject.data;
              const user = await userModel.findOne({ email });
              if (!user) {
                  return res.status(401).json({
                      message: 'Invalid email or password'
                  });
              }
    
              const comparePassword = await bcrypt.compare(password, user.password);
              if (!comparePassword) {
                  return res.status(401).json({
                      message: 'Invalid email or password'
                  });
              }
    
              const token = jwt.sign({ userId: user._id }, process.env.jwt_secret_user, { expiresIn: '24h' });
              res.status(200).json({
                  message: 'signed in successfully',
                  token
              });
          } catch (error) {
              res.status(500).json({
                  message: `something went wrong, error: ${error.message}`
              });
          }
      });
    

2. Course Management

The course management feature allows users to view and purchase courses. The /buyCourse endpoint checks if the user has already purchased the course and processes the purchase if not.

courseRouter.post('/buyCourse', purchaseRate, userAuth, async (req, res) => {
    try {
        const userId = req.userId;
        const courseId = req.body.courseId;

        // Check if the user has already purchased the course
        const isPurchase = await purchaseModel.findOne({ userId, courseId });
        if (isPurchase) {
            return res.status(403).json({
                message: 'you have bought the course already'
            });
        }

        await purchaseModel.create({ userId, courseId });
        res.status(200).json({
            message: 'successfully bought the course'
        });
    } catch (error) {
        res.status(500).json({
            message: `something went wrong: ${error.message}`
        });
    }
});

3. Course Preview

Users can browse available courses using the /preview endpoint, which returns a list of all courses.

courseRouter.get('/preview', browseRate, async (req, res) => {
    try {
        const courses = await courseModel.find({});
        res.status(200).json({ courses });
    } catch (error) {
        res.status(500).json({
            message: `something went wrong with the server: ${error.message}`
        });
    }
});

Mongoose Models

The application uses Mongoose to define schemas for the user, course, and purchase models:

  • User Model: Stores user information.

      const userSchema = new Schema({
          email: {
              type: String,
              required: true,
              lowercase: true,
              trim: true
          },
          username: {
              type: String,
              unique: true,
              required: true,
              lowercase: true,
              trim: true
          },
          password: {
              type: String,
              required: [true, "password is required"]
          },
          fullName: {
              type: String,
              lowercase: true,
              trim: true,
              required: [true, "full name is required"]
          }
      });
    
      const userModel = mongoose.model('User', userSchema);
    
  • Course Model: Contains details about each course.

      const courseSchema = new Schema({
          title: {
              type: String,
              unique: true,
              required: true
          },
          description: {
              type: String,
              required: true,
              trim: true
          },
          price: {
              type: Number,
              required: true
          },
          creatorId: ObjectId
      });
    
      const courseModel = mongoose.model('Course', courseSchema);
    
  • Purchase Model: Records user purchases associated with their respective courses.

      const purchaseSchema = new Schema({
          userId: ObjectId,
          courseId: ObjectId
      });
    
      const purchaseModel = mongoose.model('Purchase', purchaseSchema);
    

Rate Limiting

To prevent abuse of the API, the application implements rate limiting using the express-rate-limit package. This feature ensures that users can only make a limited number of requests in a given timeframe.

const purchaseRate = rateLimit({
    windowMs: 1 * 60 * 1000,
    max: 5,
    standardHeaders: true,
    legacyHeaders: false
});

Error Handling

The application includes robust error handling. For example, when a user tries to sign up with an existing email, they receive a clear error message:

if (!parsedObject.success) {
    return res.status(400).json({
        message: 'incorrect data',
        error: parsedObject.error.errors
    });
}

Conclusion

This course-selling web application demonstrates the capabilities of Node.js and Express in building a scalable and efficient backend. With user authentication, course management, and rate limiting, the application is equipped to handle a variety of user interactions effectively.

Next Steps

  • Testing: Implement unit and integration tests to ensure reliability.

  • Frontend: Build a user-friendly frontend interface using a framework like React or Vue.js.

  • Deployment: Deploy the application using platforms like Heroku, AWS, or DigitalOcean.

By following this guide, you can create a fully functional course-selling application tailored to your specific needs. Happy coding