Strictness in APIs, or "Why Hapi rocks"

You know what I love about Hapi? It's strict.

What does "strict" mean?

It can mean many things. In this case, I'm talking about letting the developer know if he does something which the framework doesn't really understand.

An example is when you have a bug in your code and try to call Hapi's reply() method twice. Hapi can't send a response twice, so it'll let you know. By doing so, it have most likely uncovered a bug in your code. In this case, you might have forgotten a return statement after some error handling logic, for instance.

The same follows through in the way it uses Joi for input validation. By setting up just a few simple Joi rules, it'll automatically spit back an error to clients if it encounters any errors in the input data. And that doesn't just mean that the input contained a string that was too long or that a number was out of range. It means it will let you know if you pass it data it doesn't know about. If it expects an object with the field "id" and "name" and you pass it "naem", it'll let you know that it has no clue what "naem" means. Instead of just ending up with a null-value in the database, the developer using the API will immediately see that he mistyped.

Joi is very expressive, and with just a few lines of lovely and chainable rule-building, you can set up everything from data types to validating that there are no duplicates in an array. This greatly reduces the complexity of your controllers, as you won't have to deal with all these edge-cases.

Another example of where this "strictness" is used is in the configuration of your Hapi-server. When I started working with Hapi, I felt it was a bit tedious setting up configuration objects for each route, instead of doing it express-style and calling methods on the server instance. My opinion on this changed quickly. The first time I put an option in the wrong place in the configuration, instead of just silently ignoring it, Hapi spit back an error saying it didn't know what it meant. This might not seem like such a big deal, but it can potentially save developers hours of debugging.

The "breaking change" factor

I recently upgraded this blog to use webpack 2. While webpack does have a pretty great migration guide, they've also taken the time to actually validate the configuration. Instead of just randomly breaking, it let me know that CommonsChunkPlugin now takes an object instead of individual arguments. It even gave an example - something along the lines of:

// If your config looked like this:
new webpack.optimize.CommonsChunkPlugin(
  'common',
  'common.js'
)

// It should now look like this:
new webpack.optimize.CommonsChunkPlugin({
  name: 'common',
  filename: 'common.js'
})

Great developer experience!

Reducing debugging time

When you're starting to use some new framework, module or application, everything is unfamiliar and it's hard to know where to start looking if you encounter a bug or have a hard time getting the thing running in the first place.

I remember working with Facebook's GraphQL library a good while ago, and struggled for quite a while trying to figure out why my code wasn't working. I kept going back and forth between the examples and my code, reducing my code more and more in order to find the problem. In the end, after hours of digging, I suddenly noticed how I had tried to implement an interface using a field name of implements which should have been interfaces.

Had the GraphQL library been stricter, it would have let me know it didn't know what the implements field was. I'm not going to say that Facebook has done a bad job with the library - quite the contrary. Strictness in javascript seems to be more of an exception than a rule. My point is, I feel that we should all strive to be a little more strict when we are writing software that is supposed to be used by others.

Performance

I'm sure some could argue that being strict can sometimes be a bottleneck. That can certainly be true in some cases, but in other cases it simply doesn't matter. Let's go through a couple of examples:

  • Server configuration. Validating the configuration might take a few milliseconds. But remember: this isn't done on every request. It's done at startup. This simply isn't going to matter in terms of performance. It will only make the lives of developers better.
  • API input validation. For certain, validating input is going to be slower than not doing so. But for obvious reasons, we can't just skip the validation. And by being stricter and more expressive when defining your input validation, you can give back meaningful errors to the developer on the other side.
  • Module argument validation. Let's say you've made a small module that renders some HTML based on some input values. One of the values that are expected is supposed to be an array. What happens if the code that consumes the module has a bug and passes in a string instead of an array? It might be that your code fails because it tries to call a method on the array. It might also start looping over the characters of the string, resulting in some really weird output. How much would it cost us to validate that the input is actually an array? About three lines of code:
if (!Array.isArray(input.thing)) {
  throw new Error('`thing` expected to be an array, got ' + typeof input.thing);
}

If you call this in hot code, it might introduce a little bit of overhead for each call - sure. It might also be unnecessary in production, if you've tested your code properly in development. It will also add some extra bytes if this is shipped to frontend clients. But we can easily work around these issues, and still get the benefit of a great developer experience:

if (process.env.NODE_ENV !== 'production') {
  if (!Array.isArray(input.thing)) {
    throw new Error('`thing` expected to be an array, got ' + typeof input.thing);
  }
}

With this small tweak, you're no longer running the code in production. Combine this with Webpacks DefinePlugin or envify for Browserify where you explicitly define process.env.NODE_ENV to be production and any decent code minification tool will understand that this is dead code and remove the code from the bundle altogether.

Summary

What can we, as developers, do to make things better? I encourage everyone to follow Hapi's example. Be strict. Validate your input. Make the developer experience awesome, save us all hours of debugging. Cheers!