Today I want to discuss Server-side validation techniques in NodeJS.




we need to install a package called express validator

npm install --save express validator

in routes

const { check } = require("express-validator");

router.post("/signup", check("email").isEmail(), authController.postSignup);

/*
here we can check about incoming fields in new middleware -> check
also we can add multiple checks like password validation, jwt validation etc
look below -> multiple validators
*/

in the controller we can get the validation outputs. (in next middleware -> postSignup)

const { validationResult } = require("express-validator");

exports.postSignup = (req, res, next) => {
  const email = req.body.email;
  const password = req.body.password;

  const errors = validationResult(req);


  if (!errors.isEmpty()) {
    console.log(errors.array());
    /*
    errors printed as an array
    error msg=>errors.array()[0].msg   => outputs Invalid value   <- we can customize this
    */
   
    //return res.status(422).json({ errors: errors.array() }); for REST

     return res.status(422).render("auth/signup", {  
 //for templating engines
      active: "reset",
      pageTitle: "Password Reset",
      errorMessage: "",
    });
  }


}



Customize massage

in router

router.post(
  "/signup",
  check("email").isEmail().withMessage("Please Enter a valid Email..!!!"),
  authController.postSignup
);


now that console log returns the error as => Please Enter a valid Email...!!!  (instead of Invalid Value)


Custom validators

router.post(
  "/signup",
  check("email", "Please Enter a valid Email..!!!")  //set the default err msg here
    .isEmail()

    .custom((value, { req }) => {
      if (value === "test@gmail.com") {
        throw new Error("THis Email is forbidden"); //we can improve this by adding blacklist in our database and cheaking forbidden once
      }
      return true;
    }),
  authController.postSignup
);


Multiple Validators
const { check, body } = require("express-validator");
//we can check many things like- > cookies , headers, query params etc

To add multiple check middleware’s ->  wrap them inside an array
router.post(
  "/signup",
  [check("email"),check("password") ]  //likewise
    ,
  authController.postSignup
);

Example
router.post(
  "/signup",
  [
    check("email", "Please Enter Valid email")
      .isEmail()

      .custom((value, { req }) => {
        if (value === "test@gmail.com") {
          throw new Error("THis Email is forbidden");
        }
        return true;
      }),
    body("password", "Plaese Enter Valid Password")
      .isLength({ min: 5 })
      .isAlphanumeric(),
  ],
  authController.postSignup
);

Checking for Field Equality
router.post(
  "/signup",
  [
    check("email", "Please Enter Valid email")
      .isEmail()

      .custom((value, { req }) => {
        if (value === "test@gmail.com") {
          throw new Error("THis Email is forbidden");
        }
        return true;
      }),
    body("password", "Plaese Enter Valid Password")
      .isLength({ min: 5 })
      .isAlphanumeric(), 
---------------------new-------------------------------
    body("confirmPassword").custom((value, { req }) => {
      if (value !== req.body.password) {
        throw new Error("Password Not Matching");
      }
      return true;
    }),
  ],
  authController.postSignup
);

Async validation ->
Reject where a user is found to that email.
const User = require("../models/user");

router.post(
  "/signup",
  [
    check("email", "Please Enter Valid email")
      .isEmail()

      .custom((value, { req }) => {

        return User.findOne({ email: value }).then((userdoc) => {
          if (userdoc) {
            return Promise.reject("Email already Exists");
          }
        });
      }),
    body("password", "Plaese Enter Valid Password")
      .isLength({ min: 5 })
      .isAlphanumeric(),
    body("confirmPassword").custom((value, { req }) => {
      if (value !== req.body.password) {
        throw new Error("Password Not Matching");
      }
      return true;
    }),
  ],
  authController.postSignup
);

keeping user Input After validation
We can send the old request email and password with the new request.

return res.status(422).render("auth/signup", {
      active: "reset",
      pageTitle: "Password Reset",
      errorMessage: errors.array()[0].msg,
      oldInputs:{email: req.body.email}
//previously entered email you can send like this
    });

how to add responsive errors to relevant fields

we can use the param value inside the error message object
we can pass the full error array to the response

return res.status(422).render("auth/signup", {
      active: "reset",
      pageTitle: "Password Reset",
     
      oldInputs:{email: req.body.email},
      validationErrors:errors.array() //we pass all the errors
    });
Then we can get and examine the param field (by iterating one by one)

<input type="text" name="email" id="email" class=<%= validationErrors.find(e=>e.param==='email') ?'invalidClz':''%>>

This finds matching email field and if founds one=> it will assign a class called invalidClz.
so now in the css we can add the class
.invalidClz{
    border-color:red
}

This can repeat to other inputs also

Sanitizing data

We can normalize the inputs -> convert to lowercase,  trim etc
.normalizeEmail()
.trim()
Also there is a security feature called XSS (cross Site Scripting) sanitization. I’ll focus on visual sanitization.

router.post(
  "/signup",
  [
    check("email") //remove the default msg-> u can pass it withMessage
      .isEmail()
      .normalizeEmail() //convert into lowecase
      .custom((value, { req }) => {
        return User.findOne({ email: value }).then((userdoc) => {
          if (userdoc) {
            return Promise.reject("Email already Exists");
          }
          return true;
        });
      }),
    
    body("password", "Plaese Enter Valid Password")
      .isLength({ min: 5 })
      .isAlphanumeric()
      .trim(), //remove white sapaces
    body("confirmPassword").custom((value, { req }) => {
      if (value !== req.body.password) {
        throw new Error("Password Not Matching");
      }
      return true;
    }),
  ],
  authController.postSignup
);