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

  1. Initialize the Project

First, create a new directory for your project and initialize it with npm:

mkdir task-manager
cd task-manager
npm init -y

  1. Install Dependencies

Install the necessary dependencies:

npm install express mongoose bcryptjs jsonwebtoken dotenv
npm install --save-dev nodemon

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

  1. Configure Environment Variables

Create a .env file in the root directory and add the following:

PORT=3000
MONGO_URI=mongodb://localhost:27017/taskmanager
JWT_SECRET=your_jwt_secret

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

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

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

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

  1. 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');
  }
};

  1. 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');
  }
};

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

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

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

  1. Start the Server

Use nodemon to start the server:

npx nodemon app.js

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

  1. Prepare for Deployment

Ensure your application is ready for deployment by setting up environment variables and configuring the database connection for production.

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

Module 6: Express.js Framework

Module 7: Databases and ORMs

Module 8: Authentication and Authorization

Module 9: Testing and Debugging

Module 10: Advanced Topics

Module 11: Deployment and DevOps

Module 12: Real-World Projects

© Copyright 2024. All rights reserved