How to Make a Task Manager API Using Node JS (and Express and MongoDB)

Abhishek Soni
The Startup
Published in
8 min readSep 26, 2020

Let's dive in by defining the functionality and the structure we are going to have.

  • Ability to create, read, update, and delete tasks.
  • User profile with CRUD operations.
  • User authentication.
  • The user profile picture is always great for personalization, so we will also cover file uploading.
  • And we want our users to be safe, so we will encrypt user passwords in the database.
  • And we would like to run operations on tasks using their id, so we will cover that as well.

Let’s structure the app source -

  • Index.js — our index app file to run.
  • Routers — We will put our users and tasks functionality into different files to reduce the complexity of our API.
  • Models —Here we will define models to store data in the database in a good structure.
  • Middleware — Initially we only have one which is authentication, but this is a good structure for future use when we might want to add more.
  • Emails — We are going to use Sendgrid to send a few automated emails to our users on certain operations.
  • DB — And our database.
  • Other than the source files, we will have our default package and git ignore(if used) file.

So the structure will look like —

Now let's dive in the real code.

Index File

Now we know the app structure and basics we need for our app so let's define them and we are going to be using JSON for data operations.

// Requiring the packages we will need in the app.const express = require(`express`);
const userRouter = require("./routers/user");
const taskRouter = require("./routers/tasks");
const mongooserequiring = require(`./db/mongoose`);
// Defining app to be an express app and the port to use, we define PORT number in our config file for security and reliability const app = express();
const port = process.env.PORT;
// Using in appapp.use(express.json());
app.use(userRouter);
app.use(taskRouter);
// Just logging the app startapp.listen(port, () => {console.log("Server is up at " + port);});

DB Setup

Also, let's define our mongoose database file.

const mongoose = require("mongoose");mongoose.connect(process.env.MONGODB_URL, {useNewUrlParser: true,useCreateIndex: true,useUnifiedTopology: true,});

Creating the User Model

Here we define the data we want in our user profile for our Mongo Database, we are using Mongoose for data modeling.

// Requiring Stuff const mongoose = require("mongoose");const validator = require("validator"); // For easy email updation, etc.const bcrypt = require("bcryptjs"); // For encrypting passwordconst jwt = require("jsonwebtoken"); // For user authentication tokenconst Task = require("./task");

Defining the Schema with Mongoose :

const userSchema = new mongoose.Schema({name: {type: String,required: true,trim: true,},age: {type: Number,},email: {type: String,unique: true,required: true,trim: true,lowercase: true,validate(value) {if (!validator.isEmail(value)) {throw new Error("Email is invalid");}},},password: {type: String,required: true,trim: true,validate(value) {if (value.length < 6) {throw new Error("Password should be more than 6 characters!");} else if (value.toLowerCase() == "password") {throw new Error("Password cannot be password, come on man!");}},},tokens: [{token: {type: String,required: true,},}, ],avatar: {type: Buffer,},}, {timestamps: true,});

We link the user to the task model and also authenticate operation of the user model as:

userSchema.virtual("tasks", {ref: "Task",localField: "_id",foreignField: "owner",});userSchema.statics.findByCredentials = async(email, password) => {const user = await User.findOne({ email });if (!user) {throw new Error("Unable to login, please check your details.");}const isMatch = await bcrypt.compare(password, user.password);if (!isMatch) {throw new Error("Unable to login, please recheck your details.");}return user;};userSchema.methods.generateAuthToken = async function() {
const user = this;const token = jwt.sign({ _id: user._id.toString() }, process.env.JWT_SECRET);user.tokens = user.tokens.concat({ token });await user.save();return token;};// Sending back user profile info, excluding some attributesuserSchema.methods.toJSON = function() {const user = this;const userObject = user.toObject();delete userObject.password;delete userObject.tokens;delete userObject.avatar;return userObject;};// Hashing the password before savinguserSchema.pre("save", async function(next) {const user = this;if (user.isModified("password")) {user.password = await bcrypt.hash(user.password, 8);}next();});
// Remove all tasks of a user, if user is deleteduserSchema.pre("remove", async function(next) {const user = this;await Task.deleteMany({ owner: user._id });next();});const User = mongoose.model("User", userSchema);module.exports = User;

So now we have our user model ready, so we can move forward to define our users.

User Route for creating a new user

Here we define the app most of the user-related functions :

// Requiring stuff that we will need in the user route file once we define all functions const express = require("express");
const sharp = require("sharp");
const router = new express.Router();
const User = require("../models/user");
const auth = require("../middleware/auth");
const multer = require("multer");
const { sendWelcomeEmail, sendCancelationEmail } = require("../emails/account");
// Creating a new user router.post( "/users", async ( req, res ) => {const user = new User( req.body );try {await user.save();sendWelcomeEmail( user.email, user.name );const token = await user.generateAuthToken();res.status( 200 ).send( { user, token } );} catch ( e ) {res.status( 400 ).send( e );}} );

Sign In and Sign Out User Routes:

router.post( "/users/login", async ( req, res ) => {try {const user = await User.findByCredentials(req.body.email,req.body.password);const token = await user.generateAuthToken();res.send( {user,token,} );} catch ( e ) {res.status( 400 ).send( {error: "Catch error",e,} );}} );router.post( "/users/logout", auth, async ( req, res ) => {try {req.user.tokens = req.user.tokens.filter( ( token ) => {return token.token !== req.token;} );await req.user.save();res.status( 200 ).send();} catch ( e ) {res.status( 500 ).send();}} );router.post( "/users/logoutAll", auth, async ( req, res ) => {try {req.user.tokens = [];await req.user.save();res.status( 200 ).send();} catch ( e ) {res.status( 500 ).send();}} );

Read, Update, and Delete operations on User profile :

router.get( "/users/me", auth, async ( req, res ) => {res.send( req.user );} );router.patch( "/users/me", auth, async ( req, res ) => {const updates = Object.keys( req.body );const allowedUpdates = [ "name", "email", "password", "age" ];const isValidOperation = updates.every( ( update ) =>allowedUpdates.includes( update ));if ( !isValidOperation ) {return res.status( 401 ).send( { error: "Invalid updates" } );}try {updates.forEach( ( update ) => ( req.user[ update ] = req.body[ update ] ) );await req.user.save();res.status( 201 ).send( req.user );} catch ( e ) {res.status( 404 ).send( {e,} );}} );router.delete( "/users/me", auth, async ( req, res ) => {try {await req.user.remove();sendCancelationEmail( req.user.email, req.user.name );res.send( req.user );} catch ( e ) {res.status( 500 ).send( { e: "Catch Error", e } );}} );

Adding, Deleting, and Fetching Avatar (Profile Picture), we are using for Multer package for uploading :

const upload = multer( {limits: {fileSize: 1000000,},fileFilter ( req, file, cb ) {if ( !file.originalname.match( /\.(jpg|jpeg|png)$/ ) ) {return cb( new Error( "Please upload jpg, jpeg or png image format file" ) );}cb( undefined, true );},} );router.post("/users/me/avatar",auth,upload.single( "avatar" ),async ( req, res ) => {const buffer = await sharp( req.file.buffer ).resize( { width: 200, height: 200 } ).jpeg().toBuffer();req.user.avatar = buffer;await req.user.save();res.send();},( error, req, res, next ) => {res.status( 400 ).send( {error: error.message,} );});router.delete( "/users/me/avatar", auth, async ( req, res ) => {req.user.avatar = undefined;await req.user.save();res.send();} );router.get( "/users/:id/avatar", async ( req, res ) => {try {const user = await User.findById( req.params.id );if ( !user || !user.avatar ) {throw new Error();}res.set( "Content-Type", "image/jpg" );res.send( user.avatar );} catch ( e ) {res.status( 404 ).send();}} );module.exports = router;

So we complete our user model here.

Task Model

We now define our task model, we are going to have operations on a task, description, and completion status, so our model is going to be simple :

const mongoose = require("mongoose");const taskSchema = new mongoose.Schema({Task: {type: String,required: true,},completed: {type: Boolean,default: false,},owner: {type: mongoose.Schema.Types.ObjectId,required: true,ref: "User",},}, {timestamps: true,});const Task = mongoose.model("Task", taskSchema);
module.exports = Task;

Now we can define our task route.

Task Route

For the creation of the task and task route requirements :

const express = require("express");const router = new express.Router();const Task = require("../models/task");const auth = require("../middleware/auth");
// Create a task
router.post("/tasks", auth, async(req, res) => {const task = new Task({...req.body,owner: req.user._id,});try {await task.save();res.status(200).send("Task saved: " + task);} catch (e) {res.status(400).send(e);}});

Reading/Requesting the tasks and sorting by date creation :

router.get("/tasks", auth, async(req, res) => {const match = {};const sort = {};if (req.query.completed) {match.completed = req.query.completed === "true";}if (req.query.sortBy) {const parts = req.query.sortBy.split(":");sort[parts[0]] = parts[1] === "desc" ? -1 : 1;}try {await req.user.populate({path: "tasks",match,options: {limit: parseInt(req.query.limit),skip: parseInt(req.query.skip),sort,},}).execPopulate();res.send(req.user.tasks);} catch (e) {res.status(500).send(e);}});

Operations using the task ID :

router.get("/tasks/:id", auth, async(req, res) => {const _id = req.params.id;try {const task = await Task.findOne({ _id, owner: req.user._id });if (!task) {return res.status(401).send({ error: "Task id not found" });}res.send(task);} catch (e) {res.status(400).send(e);}});router.patch("/tasks/:id", auth, async(req, res) => {const updates = Object.keys(req.body);const allowedUpdates = ["Task", "completed"];const isValidOperation = updates.every((update) =>allowedUpdates.includes(update));if (!isValidOperation) {return res.status(401).send({ error: "Invalid updates" });}try {const task = await Task.findOne({_id: req.params.id,owner: req.user._id,});if (!task) {res.send({ error: "Task id not found to update" });}updates.forEach((update) => (task[update] = req.body[update]));await task.save();res.send(task);} catch (e) {res.status(400).send(e);}});router.delete("/tasks/:id", auth, async(req, res) => {try {const task = await Task.findOneAndDelete({_id: req.params.id,owner: req.user._id,});if (!task) {res.status404.send({ error: "Task id not found" });}res.send(task);} catch (e) {res.status(500).send({ e: "Catch Error", e });}});module.exports = router;

And now we have the models and routes for both the users and the tasks. So we head to the middleware.

Middleware — Authentication

Here we have the auth.js middleware for the authentication of the user, which we use for user verification on almost all of our routes to verify user identity for security.

const jwt = require("jsonwebtoken");const User = require("../models/user");const auth = async(req, res, next) => {try {const token = req.header("Authorization").replace("Bearer ", "");const decoded = jwt.verify(token, process.env.JWT_SECRET);const user = await User.findOne({_id: decoded._id,"tokens.token": token,});if (!user) {throw new Error();}req.token = token;req.user = user;next();} catch (e) {res.status(403).send({error: "Please authenticate.",});}};module.exports = auth;

Emails

Our email automation with SendGrid in the account.js file.

const sgMail = require("@sendgrid/mail");sgMail.setApiKey(process.env.SENDGRID_API_KEY);const sendWelcomeEmail = (email, name) => {sgMail.send({to: email,from: "abhisheksoni1551@gmail.com",subject: "Thanks for joining in!",text: `Welcome to the app, ${name}. Let me know how you get along with the app.`,}).then(() => {console.log("Email sent success");}).catch((error) => {console.log("error", error);});};const sendCancelationEmail = (email, name) => {sgMail.send({to: email,from: "abhisheksoni1551@gmail.com",subject: "Sorry to see you go.",text: `We would love to see you back, ${name}.`,}).then(() => {console.log("Delete user email success");}).catch((error) => {console.log("error", error);});};module.exports = {sendWelcomeEmail,sendCancelationEmail,};

And in our config file, we can define details as, and save file with ‘.env’ format:

PORT=[Port number]SENDGRID_API_KEY=[YOUR KEY]MONGODB_URL=[Your Mongo URL]JWT_SECRET=[Your Secret Key]

Congratulations, we have our task manager API ready now.

If you have any queries or comments, feel free to comment or email me at abhishek@abhisheknotes.com. Happy programming.

--

--

Abhishek Soni
The Startup

Thinker, Optimist, and Engineer. Looking for SDE opportunities.