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