Search code examples
javascriptnode.jstypescriptexpressexpress-validator

express-validator returns validation errors twice


I want to validate the request object using Express-Validator. Let's assume I have two routes, a GET /users/:id (fetchUserById) and POST /users (createUser) route

this.router = express.Router();
this.router.route('/').post(this.userRequestValidator.createUser, this.userController.createUser);
this.router.route('/:id').get(this.userRequestValidator.fetchUserById, this.userController.fetchUserById);

As you can see I call the validation middleware right before calling the controller logic. First I created a base validator dealing with the validation errors and returning a HTTP 400 if something failed.

export abstract class RequestValidator {
    protected validate = async (request: Request, response: Response, next: NextFunction): Promise<void> => {
        const errors: Result<ValidationError> = validationResult(request);

        if (!errors.isEmpty()) {
            return res.status(422).json({ errors: errors.array() });
        } else {
            next();
        }
    };
}

My validator functions userRequestValidator.createUser and userRequestValidator.fetchUserById just have to extend the RequestValidator and implement the validations

export class UserRequestValidator extends RequestValidator {
    public createUser = [
        body('username')
            .isString()
            .exists(),
        body('password')
            .isString()
            .exists(),
        this.validate,
    ];

    public fetchUserById = [
        param('id')
            .isString()
            .isUUID()
            .exists(),
        this.validate,
    ];
}

When I call GET localhost:3000/users/abc I get this response

{
    "errors": [
        {
            "value": "abc",
            "msg": "Invalid value",
            "param": "id",
            "location": "params"
        }
    ]
}

This is the response I am expecting. But when I call POST localhost:3000/users with an empty body I get this response

{
    "errors": [
        {
            "msg": "Invalid value",
            "param": "username",
            "location": "body"
        },
        {
            "msg": "Invalid value",
            "param": "username",
            "location": "body"
        },
        {
            "msg": "Invalid value",
            "param": "password",
            "location": "body"
        },
        {
            "msg": "Invalid value",
            "param": "password",
            "location": "body"
        }
    ]
}

Does someone know how I can fix this behaviour or what's wrong with my setup?


Solution

  • I don't know why when req.body is a empty object - {}, the validator will run through all node of validation chain. You can check again, add each message for each condition, like as follow:

    class UserRequestValidator extends RequestValidator {
      public createUser = [
        body('username')
          .isString().withMessage('username must be a string') // you can see both error messages in the response
          .exists().withMessage('username must be exist'),
        body('password') // the same for this field
          .isString()
          .exists(),
        this.validate,
      ];
    
      public fetchUserById = [
        param('id') // because id is exist in `req.params`, then only one test has been executed.
          .isString().withMessage('id must be a string')
          .isUUID()
          .exists(),
        this.validate,
      ];
    }
    

    I found a solution for your case in https://github.com/express-validator/express-validator/issues/638 , stop chain in the first error with .bail() function.

    Then your validator class will be like:

    class UserRequestValidator extends RequestValidator {
      public createUser = [
        body('username')
           // always check exists() first
          .exists().withMessage('username must be exist').bail()
          .isString().withMessage('username must be a string').bail(),
        body('password')
          .exists().bail()
          .isString().bail(),
        this.validate,
      ];
    
      public fetchUserById = [
        param('id')
          .isString()
          .isUUID()
          .exists(),
        this.validate,
      ];
    }