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:
Node.js: Ensure Node.js is installed on your machine.
MongoDB: Have a MongoDB database ready for storing user, course, and purchase information.
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