hyte-web-dev

Input Validation and Error Handling

Data Validation and Sanitization

Input data validation in web applications is a critical process that ensures the integrity, accuracy, and security of the data received from user inputs. It involves verifying that the data entered by users matches the expected format, type, and content before processing or storing it. Effective input validation can prevent a wide range of issues, including security vulnerabilities, data corruption, and application errors.

Client-Side Validation

Server-Side Validation

Server-side data validation with Express

Example 1: User registration (POST /api/users)

(or POST /api/auth/register)

  1. Install express-validator
  2. Example database query function in user-model.js:

     /**
     * Creates a new user in the database
     * 
     * @param {object} user data
     * @returns {number} - id of the inserted user in db
     */
     const addUser = async (user) => {
       try {
         const sql = `INSERT INTO Users (username, email, password, user_level_id)
                     VALUES (?, ?, ?, ?)`;
         // user level id defaults to 2 (normal user)                 
         const params = [user.username, user.email, user.password, 2];
         const result = await promisePool.query(sql, params);
         return result[0].insertId;
       } catch (e) {
         console.error('error', e.message);
         return {error: e.message};
       }
     }
    
  3. Add validation middleware to the route handler and validation rules to the request body fields

     ...
     import {body} from 'express-validator';
     ...
     // routes for /api/users/
     userRouter.route('/')
       .get(getUsers)
       .post(
         body('email').trim().isEmail(),
         body('username').trim().isLength({min: 3, max: 20}).isAlphanumeric(),
         body('password').trim().isLength({min: 8}),
         postUser
       );
     ...
    
  4. Handle validation errors in controller

     import {validationResult} from 'express-validator';
     import {addUser} from '../models/user-model.js';
    
     const postUser = async (req, res) => {
       // validation errors can be retrieved from the request object (added by express-validator middleware)
       const errors = validationResult(req);
       // check if any validation errors
       if (!errors.isEmpty()) {
         return res.status(400).json({errors: errors.array()});
       }
       const newUserId = await addUser(req.body);
       res.json({message: 'new user added', user_id: newUserId});
     };
     ...
    
  5. Test the endpoint with Postman or VSCode REST Client
    • Try to send invalid data and verify that the validation works as expected

Example 2: Post entry (POST /api/entries)

  1. Add validation middleware to the route handler and validation rules to the request body fields

     ...
     import {body} from 'express-validator';
     ...
     entryRouter
       .route('/')
       .get(getEntries)
       .post(
         authenticateToken,
         // TODO: add validation rules here
         postEntry);
     ...
    
  2. Handle validation errors in controller

     import {validationResult} from 'express-validator';
     ...
     const postEntry = async (req, res) => {
    
       // validation errors can be retrieved from the request object (added by express-validator middleware)
       const errors = validationResult(req);
       // check if any validation errors
       if (!errors.isEmpty()) {
         return res.status(400).json({errors: errors.array()});
       }
       // TODO: call model function to add the valid entry item to database
       ...
     };
     ...
    
  3. Test the endpoint with Postman or VSC REST Client

    • Try to send invalid data and verify that the validation works as expected

Handling Errors in Express

  1. Add error handler middleware functions to middlewares/middlewares.js (or create a new file middlewares/error-handler.js):

     ...
     const notFoundHandler = (req, res, next) => {
       const error = new Error(`Not Found - ${req.originalUrl}`);
       error.status = 404;
       next(error); // forward error to error handler
     };
     /**
     * Custom default middleware for handling errors
     */
     const errorHandler = (err, req, res, next) => {
       res.status(err.status || 500); // default is 500 if err.status is not defined
       res.json({
         error: {
           message: err.message,
           status: err.status || 500
         }
       });
     };
     ...
    
  2. Import the error handler middleware in index.js and add it as the last middleware in the chain

     ...
     // Import error handler middlewares on the top of the file
     ...
     ...
     // Default for all routes not handled by routers above
     app.use(notFoundHandler);
     // Add error handler middleware as the last middleware in the chain
     app.use(errorHandler);
     ...
    
  3. Refactor your code to use the error handler middleware
    • Remove the error responses from your controllers
    • Use next() to pass the error to the error handler middleware, some examples:
     // user-controller.js
     import {validationResult} from 'express-validator';
     import {addUser} from '../models/user-model.js';
    
     const postUser = async (req, res, next) => {
       // validation errors can be retrieved from the request object (added by express-validator middleware)
       const errors = validationResult(req);
       // check if any validation errors
       if (!errors.isEmpty()) {
         // pass the error to the error handler middleware
         const error = new Error('Invalid or missing fields');
         error.status = 400;
         return next(error);
       }
       // TODO: add password hashing here and error handling for SQL errors
       const newUserId = await addUser(req.body);
       res.json({message: 'new user added', user_id: newUserId});
     };
     ...
    
     // entry-controller.js
     const postEntry = async (req, res, next) => {
       const errors = validationResult(req);
       // check if any validation errors
       if (!errors.isEmpty()) {
         console.log('postEntry errors', errors.array());
         const error = new Error('Invalid or missing fields');
         error.status = 400;
         return next(error);
       }
       // get fields from request body
       const {entry_date, mood, weight, sleep_hours, notes} = req.body;
       // req.user is added by authenticateToken middleware
       const user_id = req.user.user_id;
       // combine fields into a new entry object
       const newEntry = {user_id, entry_date, mood, weight, sleep_hours, notes};
       const result = await addEntry(newEntry);
       if (result.error) {
         return next(new Error(result.error));
       }
       res.status(201).json({message: 'New entry added.', ...result});
     };
     ...
    
    • or instead of handling the validation errors in every controller where needed, you can create a generic validation error handler middleware and use it in the routes:
     // middlewares/error-handler.js
     /**
     * Custom middleware for handling and formatting validation errors
     * @param {object} req - request object
     * @param {object} res - response object
     * @param {function} next - next function
     * @return {*} next function call
     */
     const validationErrorHandler = (req, res, next) => {
       const errors = validationResult(req, {strictParams: ['body']});
       if (!errors.isEmpty()) {
         // console.log('validation errors', errors.array({onlyFirstError: true}));
         const error = new Error('Bad Request');
         error.status = 400;
         error.errors = errors.array({onlyFirstError: true}).map((error) => {
           return {field: error.path, message: error.msg};
         });
         return next(error);
       }
       next();
     };
     ...
    
     ---
    
     // example usage in user registration route
     import {validationErrorHandler} from '../middlewares/error-handler.js';
     ...
     userRouter.route('/').post(
       body('username', 'username must be 3-20 characters long and alphanumeric')
         .trim()
         .isLength({min: 3, max: 20})
         .isAlphanumeric(),
       body('password', 'minimum password length is 8 characters')
         .trim()
         .isLength({min: 8, max: 128}),
       body('email', 'must be a valid email address')
         .trim()
         .isEmail()
         .normalizeEmail(),
       validationErrorHandler,
       postUser,
     );
    
     // user-controller.js
     import {validationErrorHandler} from '../middlewares/error-handler.js';
     ...
     userRouter.route('/')
       .get(getUsers)
       .post(
         validationErrorHandler,
         postUser
       );
     ...
    
  4. Finally, test the error handler by sending different kind of invalid requests to the API, for example:
    • POST /api/users with an empty request body
    • POST /api/users with invalid email address/username/password
    • POST /api/entries with an empty request body
    • POST /api/entries with some invalid field values

Assignment - Input validation and error handling

  1. Continue your existing Express app and create a new branch validation
    • See the teacher’s example (link in Oma) for reference to get started
  2. Implement error handler middleware
    • Use the error handler in your controllers instead of “hard-coded” sending error responses
  3. Implement proper server-side validation and sanitization for input data
    • Use express-validator
    • Specify the validation rules for each field in the request bodies

Returning: See related assignment in Oma.