Server and Client Side Validation with JavaScript, HTML, and Hapi

When accepting any form of user input it’s necessary to validate the submitted information in order to ensure its accuracy and validity. Reasons for this include but are not limited to:

  • A user may forget to fill a field or enter information in the wrong place, and relying on invalid information on your server will lead to pain, anguish, and suffering
  • A malicious user may attempt to inject executable code into your server through a form
  • Validating a form allows you to enforce rules such as password length, which can help protect you and your users

Validating input is obviously a necessity, so the question is really about how and where it should occur. Throughout my time at Codesmith I’ve learned some techniques for validation which attempt to solve the problem securely while emphasizing user experience.

Client-side Validation

The first step in validation takes place on the client-side. Using a combination of Javascript and HTML it’s possible to provide fairly robust validation rules. The main advantage of client-side validation is that it requires no HTTP request/response cycle since it all takes place on the client’s machine, and thus near instantaneous feedback may be provided. This allows for a very responsive and clear user experience. Let’s take an example form:

<form action="/payment" method="POST" id="payment-form">
<input type="text" name="username" required />
<input type="email" name="email" required />
<input type="number" name="age" />
<input type="submit" value="submit" />
</form>

HTML 5 provides some very handle validations right out of the box. Notice in the code above the type and the required properties of the input fields. If a user skips a required field, enters an invalid email address into the email field, or enters anything other than a number into the age field, then they’ll receive a very informative visual notification of their error and the form will not submit.

Here’s a different form with an Email validation

The pattern property allows you to define a regular expression that the input must conform to. The title property then allows you to specify the message to be displayed if the validation fails.

<input type="text" name="username" pattern="^[a-z0-9]{5,15}$" title="Usernames may only contain letters and numbers and must be between 5 and 15 characters">

The resulting failed validation will display a helpful message to the user.

Check out the Mozilla Developers Network page on HTML 5 Constraint Validation for the full list of available constraints.

There are some client-side constraints we may need to apply to our input that HTML can’t handle. In order to validate a form using JavaScript we’ll add an event listener to the form so that we can execute some JavaScript before form submission. This may be done using JQuery or with regular JS:

// JQuery:
$('#payment-form').submit(function() {
// Validate form here
}
// Javascript:
document.getElementById('payment-form').addEventListener('submit', function() {
// Validate form here
}

A typical use-case of JavaScript validation might be for credit card information. Using JQuery along with Stripe’s JQuery payments library makes this a cinch. We simply grab the form info before it’s submitted

$('#payment-form').submit(function() {
// Stop the form from submitting until validation occurs
var valid = true;
e.preventDefault();
  // If card number or CVC code are invalid, notify the user
if (!$.payment.validateCardNumber($('#card-num').val())) {
$('#card-num').addClass('invalid');
valid = false;
}
if (!$.payment.validateCardCVC($('#card-CVC').val())) {
$('#card-CVC').addClass('invalid');
valid = false;
}
  // Submit the form if all went well
if (valid) {
$(this).get(0).submit();
}
}

While client-side validation can provide a responsive, intuitive and smooth user experience, it’s necessary to remember that it’s not reliable and adds no security as it can be easily turned off by the user if they simply disable JavaScript or remove the HTML validations using standard developer tools. Also keep in mind that HTML5 validations are not supported by many older browsers and won’t work in IE 9 and earlier.

Server-Side Validation

Server-side validation is the consistent, secure validation that we trust will execute without fail on all input. Since the server can’t execute its validation until it’s received an HTTP request, the client will have to wait for a response from the server before displaying any sort of success or error message to the user. Typically this is acceptable, but when forging the best user experience possible it’s nice to have some validation on the client-side before it hits the server.

Hapi makes validating payloads (post data), query parameters, path parameters, and headers incredibly straightforward and flexible. Here I have a typical Hapi route which matches to POST requests at the path /signup. If I were to circumvent the client-side validation and post to this route with the email address “foo” and an empty name string then Hapi would by default provide no validation and this data would be accepted:

server.route({
method: 'POST',
path: '/signup',
handler: function(request, reply) {
console.log(request.payload.email); // 'foo'
console.log(request.payload.name); // ''
}
});

Allowing this invalid data into our application could cause unpredictable results at a later date, especially if we want to email our customers, so let’s have Hapi validate this data before we do anything with it. To do so simply add the config, validate, and payload keys to your route configuration like so:

server.route({
method: 'POST',
path: '/signup',
handler: function(request, reply) {
console.log(request.payload.email);
console.log(request.payload.name);
},
config: {
validate: {
payload: {
name: Joi.string().required(),
email: Joi.string().email().required()
}
}
}
});

Now when our route accepts a post request our handler function won’t be called unless the name and email parameters exist in the payload (body) of the post. Each must be a string and the email address must be of a valid email format. Notice that the validation logic is in the form a Joi object. Joi provides a very clear and readable syntax for creating complex validations. I invite you to check out their docs to learn more of the many powerful validation options available to you.

Hapi also provides validation for path parameters, inputs, and query strings. Check out their their Validation docs for more info.

At this point our server won’t allow invalid email address or names, but it’s not providing a great user experience. When I post to the /signup route without the required name field Hapi simply responds with a 400 error and a somewhat descriptive JSON object.

There are a few options here for gracefully handling this failure. If you’re making your post request using JavaScript or JQuery then you can parse through the response data and write an appropriate error message to the DOM.

$.post('/signup', JSON.stringify({name: '', email: 'foo'}), success)
.error(function(err) {
// Catch the error and do something helpful with the error
console.log(err.status); // logs '400'
}
);
function success(data) { ... }

Hapi also provides us with the ability to run a custom validation function in which custom server code may be run before or after the validation. We can even alter the error message if desired by creating a new Error object and passing it along to the next function.

server.route({
method: ‘POST’,
path: ‘/signup’,
handler: function(request, reply) {...},
config: {
validate: {
payload: function (value, options, next) {
var schema = {
name: Joi.string().required(),
email: Joi.string().email().required(),
};
Joi.validate(value, schema, function(err, value) {
          // Perform custom actions
if (err) err = new Error("We done goofed.");
          next(err, value);
});
}
}
}
}

Notice the signature of our function is (value, options, next). Hapi will pass the contents of the payload into the value parameter, any server-wide validation options into the options parameter, and the next callback function in the chain into the next parameter. The next function must be passed an error message object (or null if no error) and the original provided value (in this case the original payload/request body).

You may decide you’d like to redirect to a helpful error page in the case of a validation error. The best way I’ve found to do this is by extending the Hapi server with an onPreResponse extension. This function will run before the server sends every reply, so we can simply check there to see if there’s an error with the request before sending a reply. If there’s an error we show a custom error page, otherwise we allow the normal reply flow to continue by calling reply.continue().

server.ext(‘onPreResponse’, function(request, reply) {
// if there's no error then just send the normal response
if (!request.response.isBoom) return reply.continue();
  // get path, code, and error information from the request
var path = request._route.path,
code = request.response.output.payload.statusCode,
error = request.response.output.payload.message;
  // show a custom error page based on the path
if (path === ‘/signup’) {
return reply.view(‘signup-error').code(code);
}

// show a standard error page for any other errors
reply.view(‘error’).code(400); // 400 code means Bad Request
});

Thanks for reading and I hope you learned something about validation today!