For the last few days, I've been working on an API for Kids In Touch using Hapi.js from the folks at Walmart Labs. I'm pretty impressed with it so far. I have found it to be much easier to adapt that Express. Hapi some great default configurations that make it very easy to build an API.

One thing that I wasn't too happy about was the default output from validations on a route. I felt the output was overly verbose and hard to parse on the client.

Fortunately, Hapi's validation system provides a "failAction" property. So, you can just take the original validation output and format it according to your API's specs.

Here is an example of the default output:

{
    "code": 400,
    "error": "Bad Request",
    "message": "the length of familyName must be at least 2 characters long, the value of givenName is not allowed to be undefined, the value of mobile is not allowed to be undefined",
    "validation": {
        "source": "payload",
        "keys": [
            "familyName",
            "givenName",
            "mobile"
        ]
    }
}

Whew! A client would need to do a lot of parsing just to figure out what fields to mark as invalid and for what reason. It would be even more difficult when a single field causes more than one validation error.

To pretty this all up a bit, here is a validation formatter that will modify the output. Just save this as a file in the application root and then use it for the fail action on every route.

var _ = require("lodash");

/**
 * A mapping of errors produced by Hapi/Joi to errors desired in API
 * @type {Array}
 */
var errorMessageMaps = [
    { newMessage : "maximum-length",    matches : "less than"   },
    { newMessage : "minimum-length",    matches : "at least"    },
    { newMessage : "true-required",     matches : "true"        },
    { newMessage : "false-required",    matches : "false"       },
    { newMessage : "invalid-email",     matches : "email must be a valid email" },
    { newMessage : "required",          matches : "undefined"   },
    { newMessage : "not-allowed",       matches : "the key"     }
];

/**
 * Find all the messages that are related to a given field
 * @param messages
 * @param field
 * @returns {Array}
 */
var findMessagesForField = function( messages, field ) {

    var matchingErrors = _.filter( messages, function( message ) {
        return  message.indexOf( field ) > 0 ? true : false ;
    });

    return matchingErrors
};

/**
 * Associate a list of errors (Hapi/Joi versions) to those desired for the API
 * @param errors
 * @returns {Array}
 */
var associateErrorMessages = function( errors ) {

    var associatedErrors = [];

    // For each error find the associated error.
    _.map( errors, function( error ) {

        var tempErrors = _.filter( errorMessageMaps, function( errorMap ) {

            return  error.indexOf( errorMap.matches ) > 0 ? true : false ;
        });

        _.map( tempErrors, function( anError ) {
            associatedErrors.push( anError.newMessage);
        })
    });

    // If there are more than one set of errors for a field that is required,
    // don't need to show any other than "required".  Example if email was not provided and
    // is required, might have ["invalid-email", "required"].  Don't need "invalid-email" error
    // because we know it was not provided.
    if( associatedErrors.indexOf( 'required') !== -1 ) {
        associatedErrors = ["required"];
    }

    return associatedErrors;

};

/**
 * Return array object that contain the invalid field and any related errors for that field.
 * @param errorInfo
 * @returns {Array}
 *
 * @example
 * [
    {
        "attr": "familyName",
        "msgs": [
            "required"
        ]
    },
    {
        "attr": "givenName",
        "msgs": [
            "required"
        ]
    }
   ]
 */

module.exports = function( errorInfo ) {

    var formatted = [];

    var messages = errorInfo.message.split(",");

    var fieldsWithErrors = errorInfo.validation.keys;
    fieldsWithErrors = _.uniq(fieldsWithErrors);

    _.map( fieldsWithErrors, function( currentField ) {

        var matchingErrors = findMessagesForField( messages, currentField );
        var associatedMessages = associateErrorMessages( matchingErrors );

        formatted.push( { "attr" : currentField , "msgs" : associatedMessages  } );

    });

    return formatted;
}

In your route, do the following:

validate : {

    payload : schema,

    // Override the default fail action for validation issues.
    // Format properly
    failAction: function( source, error, next) {
        var formattedErrors = FormatErrors( error.response.payload);


        var error = Hapi.error.badRequest();
        error.reformat();
        error.response.payload.status = 'fail';
        error.response.payload.data = formattedErrors;

        return next(error);
    }
}

Then, you'll get a nice pretty output such as :

{
    "code": 400,
    "error": "Bad Request",
    "status": "fail",
    "data": [
        {
            "attr": "familyName",
            "msgs": [
                "minimum-length"
            ]
        },
        {
            "attr": "givenName",
            "msgs": [
                "required"
            ]
        },
        {
            "attr": "mobile",
            "msgs": [
                "required"
            ]
        }
    ]
}