Getting Started with Full Stack Web Development - MERN Stack

Photo by Andrew Neel on Unsplash

Getting Started with Full Stack Web Development - MERN Stack

Part 2 Creating a functional TODO APP

Table of contents

In the previous part of this series, we set up our project and got ourselves ready for implementing various actions in our controllers folder. Check part 1 here

In the controllers folder, create todoController.js,

const asyncHandler = require('express-async-handler');
const Todo = require('../models/todoModel');

// Get all todos
// method: GET
// @desc /api/todos
// @access private

const getAllTodos = asyncHandler(async (req, res) => {
  const todos = await Todo.find()
  res.status(200).json(todos)
})

The code above controls the action that gets all todos in our database, which is a GET method with route /api/todos and a private access, that is, only the user signed in will be able to access, which will be implemented later. We will define the remaining actions following similar pattern.

// Create todo
// method: POST
// @desc /api/todos
//@access private

const createTodo = asyncHandler(async (req, res) => {
  if (!req.body.task) {
    res.status(400)
    throw new Error("Please add a task")
   }
  const todo = await Todo.create({
    task: req.body.task
  })
  res.status(200).json(todo)  
})

// Update Todo
// method: PUT
// @desc /api/todos/id
//@access private

const updateTodo = asyncHandler(async (req, res) => {
  const todo = await Todo.findById(req.params.id)

  if (!todo) {
    res.status(400)
    throw new Error("Todo does not exist")
  }

  const updatedTodo = await Todo.findByIdAndUpdate(req.params.id, req.body, {new: true})
  res.status(200).json(updatedTodo)
})

// Delete Todo
// method: DELETE
// @desc /api/todos/id
// @access private
const deleteTodo = asyncHandler(async (res, res) => {
  const todo = await Todo.findById(req.params.id)

  if (!todo) {
    res.status(400)
    throw new Error(`Todo with id of ${req.params.id} does not exist`)
  }
  todo.remove()
  res.status(201).json(`Todo with id of ${req.params.id} deleted successfully`)
})

module.exports = {
  getAllTodos,
  createTodo,
  updateTodo,
  deleteTodo
}

Now that we are done defining our actions, we have to bring them into todoRoutes, we update todoRoutes.js

const express = require('express');
const router = express.Router()

const { getAllTodos, createTodo, updateTodo, deleteTodo } = require('../controllers/todoController')

router.route('/').get(getAllTodos).post(createTodo)
router.route('/:id').put(updateTodo).delete(deleteTodo)

module.exports = router

Add these two lines of code to server.js, they are built-in middleware function in Express that allows incoming request to be parsed with JSON payload and urlencoded payload respectively.

app.use(express.json())
app.use(express.urlencoded({extended: false}))

Our various routes should be working perfectly now together with their respective actions.

You can test the defined routes by using postman, an API platform for developers to design, build test and iterate their APIs.

postman1.PNG

The above is a GET request made to http://localhost:5000/api/todos, giving the response of an empty array since we are yet to create any todo.

Let's try creating a todo, we send a POST request to the same URL http://localhost:5000/api/todos postman2.PNG

Try creating various todo, delete and update some todos using postman

Protecting our routes

Now that our todo routes are ready, we can now add protection to each route since we want each user to access just their own todos.

First, we are going to create a model for our user(store user details in our database), create a new file in models folder userModel.js,

We need the name of the user, the email which will be unique for each user and their password.

const mongoose = require('mongoose')

const userSchema = mongoose.Schema({
  name: {
    type: String,
    required: true,
  },
  email: {
    type: String,
    required: true,
    unique: true,
    match: /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]      
    {1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/
  },
  password: {
    type: String,
    required: true
  }
}, {
  timestamps: true
})

module.exports = mongoose.model('User', userSchema)
  • We need to define actions for our user, where a user can register and sign in, create a new file userController.js in controllers folder,

In our userSchema, we need the password of the user, but we don't want to store the user's password that was given to us as this is a big security risk, we need to store the encrypted version of the user's password to our database, for this we need to install a package bcrypt, a password hashing function that allows for hashing and comparing hashed passwords. Run npm install bcrypt to install bcrypt.

We also need a way to authenticate users, so that when a particular user can be verified whenever they want to access a protected route. For this, we use a token(a piece of security that may be used for authorization). One of the popular packages used to generate a token in nodeJS is the jsonwebtoken, install by running npm install jsonwebtoken . In userController.js,

const asyncHandler  = require('express-async-handler')
const bcrypt = require('bcrypt)
const User = require('../models/userModel')
const jwt = require('jsonwebtoken')

const userSignup = asyncHandler (async (req, res) => {
  const { name, email, password } = req.body
  // bcrypt section
  const salt = await bcrypt.genSalt(10)
  const hashPassword = await bcrypt.hash(password, salt)

  const userExists = await User.findOne({email})

  if (!name || !email || !password) {
    res.status(400)
    throw new Error("No empty fields allowed")
  } 

  if (userExists) {
    res.status(400)
    throw new Error("User exists already")
  }

  const validate = /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9] 
  {1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/

  if (!validate.test(email)) {
    res.status(400)
    throw new Error("Enter correct email")
  }

  const newUser = new User({
    name,
    email,
    password: hashPassword
  })

  newUser.save()

  res.status(200).json({
    _id: newUser.id,
    name: newUser.name,
    email: newUser.email,
    token: generateToken(newUser.email)
  })

})

const userSignin = asyncHandler(async (req, res) => {
  const { email, password } = req.body

  const user = await User.findOne({email})

  if (!user) {
    res.status(404)
    throw new Error("User does not exist, please register")
  }

  const isPasswordCorrect = await bcrypt.compare(password, user.password)

  if (user && isPasswordCorrect) {
    res.status(200).json({
      name: user.name,
      email: user.email,
      token: generateToken(user.email)
    })
  } else {
      res.status(400)
      throw new Error("Enter correct credentials")
  }
})

const generateToken = (email) => {
  //jsonwebtoken section
  return jwt.sign({email}, process.env.JWT_SECRET, {expiresIn: '30d'})
}

module.exports = {
  userSignup,
  userSignin
}

Add a new environment variable to .env, the JWT_SECRET is peculiar to jsonwebtoken, you can use any value

JWT_SECRET = 1234

Now that various actions have been defined for user, we need to be able to reference user in our todo model, in todoModel.js,

const mongoose = require('mongoose');

const todoSchema = mongoose.Schema({
  user: {
    type: mongoose.Schema.Types.ObjectId,
    ref: 'User',
    required: true
  },
  task: {
    type: String,
    required: true
  },
  completed: {
    type: Boolean,
    required: true,
    default: false
  }
}, {
  timestamps: true
})

module.exports = mongoose.model('Todo', todoSchema)

We then create userRoutes.js in routes folder to define routes for user actions,

const express = require('express')
const router = express.Router()
const { userSignup, userSignin } = require('../controllers/userController')

router.post('/', userSignup)
router.post('/login', userSignin)

module.exports = router

We bring this into server.js by adding the code below

app.use('/api/user', require(./routes/userRoutes))

To finish up our backend, we need to protect our routes by using the token generated. For this we need to create a middleware authMiddleware.js in middlewares folder,

const jwt = require('jsonwebtoken')
const asyncHandler = require('express-async-handler')
const User = require('../models/userModel')

const protect = asyncHandler (async (req, res, next) => {
  let token
  try {
    if (req.headers && req.headers.authorization.startsWith('Bearer')) {
      token = req.headers.authorization.split(' ')[1]

      const decoded = jwt.verify(token, process.env.JWT_SECRET)

      req.user = await User.findOne({email: decoded.email}).select('-password')
      next()
    }
  } catch {
    res.status(404)
    throw new Error("Not authorized")
  }

  if (!token) {
    res.status(401)
    throw new Error("Not authorized, no token sent")
  }
})

module.exports = protect

We now use this middleware to protect all our routes that are of private access, in todoRoutes, we pass this middleware to the route,

const protect = require('../middlewares/authMiddleware')

router.route('/').get(protect, getAllTodos).post(protect, createTodo)
router.route('/:id').put(protect, updateTodo).delete(protect, deleteTodo)

We are finally done with our backend, in the next part of this tutorial, we will start by implementing our frontend

Click here for part 3

All Codes can be found on here