In this module, we will build a task management tool using Node.js and Express.js. This project will help you consolidate your knowledge of Node.js, Express, MongoDB, and other technologies covered in this course. By the end of this module, you will have a fully functional task management application that allows users to create, read, update, and delete tasks.
Table of Contents
Project Setup
- Initialize the Project
First, create a new directory for your project and initialize it with npm:
- Install Dependencies
Install the necessary dependencies:
- Project Structure
Create the following directory structure:
task-manager/ ├── config/ │ └── db.js ├── controllers/ │ └── taskController.js │ └── userController.js ├── models/ │ └── task.js │ └── user.js ├── routes/ │ └── taskRoutes.js │ └── userRoutes.js ├── middleware/ │ └── auth.js ├── .env ├── app.js └── package.json
Setting Up Express and MongoDB
- Configure Environment Variables
Create a .env
file in the root directory and add the following:
- Connect to MongoDB
Create a db.js
file in the config
directory to handle the database connection:
// config/db.js const mongoose = require('mongoose'); const dotenv = require('dotenv'); dotenv.config(); const connectDB = async () => { try { await mongoose.connect(process.env.MONGO_URI, { useNewUrlParser: true, useUnifiedTopology: true, }); console.log('MongoDB connected'); } catch (error) { console.error(error.message); process.exit(1); } }; module.exports = connectDB;
- Set Up Express
Create an app.js
file in the root directory to set up the Express server:
// app.js const express = require('express'); const connectDB = require('./config/db'); const dotenv = require('dotenv'); dotenv.config(); const app = express(); // Connect to database connectDB(); // Middleware app.use(express.json()); // Routes app.use('/api/tasks', require('./routes/taskRoutes')); app.use('/api/users', require('./routes/userRoutes')); const PORT = process.env.PORT || 3000; app.listen(PORT, () => { console.log(`Server running on port ${PORT}`); });
Creating the Task Model
- Define the Task Schema
Create a task.js
file in the models
directory:
// models/task.js const mongoose = require('mongoose'); const TaskSchema = new mongoose.Schema({ title: { type: String, required: true, }, description: { type: String, }, status: { type: String, enum: ['pending', 'in-progress', 'completed'], default: 'pending', }, user: { type: mongoose.Schema.Types.ObjectId, ref: 'User', required: true, }, createdAt: { type: Date, default: Date.now, }, }); module.exports = mongoose.model('Task', TaskSchema);
- Define the User Schema
Create a user.js
file in the models
directory:
// models/user.js const mongoose = require('mongoose'); const bcrypt = require('bcryptjs'); const UserSchema = new mongoose.Schema({ name: { type: String, required: true, }, email: { type: String, required: true, unique: true, }, password: { type: String, required: true, }, createdAt: { type: Date, default: Date.now, }, }); // Hash password before saving UserSchema.pre('save', async function (next) { if (!this.isModified('password')) { return next(); } const salt = await bcrypt.genSalt(10); this.password = await bcrypt.hash(this.password, salt); next(); }); module.exports = mongoose.model('User', UserSchema);
Building the API Endpoints
- Task Controller
Create a taskController.js
file in the controllers
directory:
// controllers/taskController.js const Task = require('../models/task'); // @desc Get all tasks // @route GET /api/tasks // @access Private exports.getTasks = async (req, res) => { try { const tasks = await Task.find({ user: req.user.id }); res.json(tasks); } catch (error) { res.status(500).send('Server Error'); } }; // @desc Create a task // @route POST /api/tasks // @access Private exports.createTask = async (req, res) => { const { title, description, status } = req.body; try { const newTask = new Task({ title, description, status, user: req.user.id, }); const task = await newTask.save(); res.json(task); } catch (error) { res.status(500).send('Server Error'); } }; // @desc Update a task // @route PUT /api/tasks/:id // @access Private exports.updateTask = async (req, res) => { const { title, description, status } = req.body; try { let task = await Task.findById(req.params.id); if (!task) { return res.status(404).json({ msg: 'Task not found' }); } // Ensure user owns task if (task.user.toString() !== req.user.id) { return res.status(401).json({ msg: 'Not authorized' }); } task = await Task.findByIdAndUpdate( req.params.id, { $set: { title, description, status } }, { new: true } ); res.json(task); } catch (error) { res.status(500).send('Server Error'); } }; // @desc Delete a task // @route DELETE /api/tasks/:id // @access Private exports.deleteTask = async (req, res) => { try { let task = await Task.findById(req.params.id); if (!task) { return res.status(404).json({ msg: 'Task not found' }); } // Ensure user owns task if (task.user.toString() !== req.user.id) { return res.status(401).json({ msg: 'Not authorized' }); } await Task.findByIdAndRemove(req.params.id); res.json({ msg: 'Task removed' }); } catch (error) { res.status(500).send('Server Error'); } };
- User Controller
Create a userController.js
file in the controllers
directory:
// controllers/userController.js const User = require('../models/user'); const bcrypt = require('bcryptjs'); const jwt = require('jsonwebtoken'); // @desc Register a user // @route POST /api/users/register // @access Public exports.registerUser = async (req, res) => { const { name, email, password } = req.body; try { let user = await User.findOne({ email }); if (user) { return res.status(400).json({ msg: 'User already exists' }); } user = new User({ name, email, password, }); await user.save(); const payload = { user: { id: user.id, }, }; jwt.sign( payload, process.env.JWT_SECRET, { expiresIn: '1h' }, (err, token) => { if (err) throw err; res.json({ token }); } ); } catch (error) { res.status(500).send('Server Error'); } }; // @desc Authenticate user & get token // @route POST /api/users/login // @access Public exports.loginUser = async (req, res) => { const { email, password } = req.body; try { let user = await User.findOne({ email }); if (!user) { return res.status(400).json({ msg: 'Invalid credentials' }); } const isMatch = await bcrypt.compare(password, user.password); if (!isMatch) { return res.status(400).json({ msg: 'Invalid credentials' }); } const payload = { user: { id: user.id, }, }; jwt.sign( payload, process.env.JWT_SECRET, { expiresIn: '1h' }, (err, token) => { if (err) throw err; res.json({ token }); } ); } catch (error) { res.status(500).send('Server Error'); } }; // @desc Get logged in user // @route GET /api/users/me // @access Private exports.getMe = async (req, res) => { try { const user = await User.findById(req.user.id).select('-password'); res.json(user); } catch (error) { res.status(500).send('Server Error'); } };
- Task Routes
Create a taskRoutes.js
file in the routes
directory:
// routes/taskRoutes.js const express = require('express'); const { getTasks, createTask, updateTask, deleteTask } = require('../controllers/taskController'); const auth = require('../middleware/auth'); const router = express.Router(); router.route('/') .get(auth, getTasks) .post(auth, createTask); router.route('/:id') .put(auth, updateTask) .delete(auth, deleteTask); module.exports = router;
- User Routes
Create a userRoutes.js
file in the routes
directory:
// routes/userRoutes.js const express = require('express'); const { registerUser, loginUser, getMe } = require('../controllers/userController'); const auth = require('../middleware/auth'); const router = express.Router(); router.post('/register', registerUser); router.post('/login', loginUser); router.get('/me', auth, getMe); module.exports = router;
- Authentication Middleware
Create an auth.js
file in the middleware
directory:
// middleware/auth.js const jwt = require('jsonwebtoken'); const dotenv = require('dotenv'); dotenv.config(); module.exports = function (req, res, next) { const token = req.header('x-auth-token'); if (!token) { return res.status(401).json({ msg: 'No token, authorization denied' }); } try { const decoded = jwt.verify(token, process.env.JWT_SECRET); req.user = decoded.user; next(); } catch (error) { res.status(401).json({ msg: 'Token is not valid' }); } };
Testing the Application
- Start the Server
Use nodemon
to start the server:
- Test Endpoints
Use a tool like Postman to test the API endpoints:
- Register User:
POST /api/users/register
- Login User:
POST /api/users/login
- Get Logged In User:
GET /api/users/me
- Get All Tasks:
GET /api/tasks
- Create Task:
POST /api/tasks
- Update Task:
PUT /api/tasks/:id
- Delete Task:
DELETE /api/tasks/:id
Deploying the Application
- Prepare for Deployment
Ensure your application is ready for deployment by setting up environment variables and configuring the database connection for production.
- Deploy to Heroku
Follow the steps in the Deploying to Heroku section to deploy your application to Heroku.
Conclusion
In this module, you have built a complete task management tool using Node.js, Express, and MongoDB. You have learned how to set up a project, create models, build API endpoints, implement authentication, and deploy the application. This project has provided you with hands-on experience in building a real-world application, reinforcing the concepts covered in this course.
Node.js Course
Module 1: Introduction to Node.js
Module 2: Core Concepts
Module 3: File System and I/O
Module 4: HTTP and Web Servers
Module 5: NPM and Package Management
- Introduction to NPM
- Installing and Using Packages
- Creating and Publishing Packages
- Semantic Versioning
Module 6: Express.js Framework
- Introduction to Express.js
- Setting Up an Express Application
- Middleware
- Routing in Express
- Error Handling
Module 7: Databases and ORMs
- Introduction to Databases
- Using MongoDB with Mongoose
- Using SQL Databases with Sequelize
- CRUD Operations
Module 8: Authentication and Authorization
Module 9: Testing and Debugging
- Introduction to Testing
- Unit Testing with Mocha and Chai
- Integration Testing
- Debugging Node.js Applications
Module 10: Advanced Topics
Module 11: Deployment and DevOps
- Environment Variables
- Using PM2 for Process Management
- Deploying to Heroku
- Continuous Integration and Deployment