A key to a good API design is good control of what data your controllers or API end points accepts. Another important key to a good design — is that you can take a brief look at your API end point and immediately understand how your request should look.
Now, of course you can validate all the request parameters inside the API end point itself. But this makes your end point messy and sort of violates the SRP (your end point should not validate requests. It should only work with them and provide a response). Now, if you are old fashioned and work with XML requests / response, you can of course use XSD to validate your incoming requests. But we are in 2016 where JavaScript runs in your browser, your desktop environment, your media center, you smart watch, your smart home and soon in your brain implants. So we gonna use JSON!
But, how do I validate JSON? I’m glad you’ve asked. Meet joi by hapijs. This small and awesome library can validate your JavaScript objects. It can do all kind of stuff like making sure that a specific key has only specific set of values, or that specific key is only required if another key exists and contains a specific value. So as always, RTFM.
We now have joi to validate our JSONs. But we still need to find an elegant way to put it inside the life cycle of our specific API end point. I’m too lazy to create a specific, generic NPM module so I’ve just dumped the current code I use in my project to a gist. Feel free to use it or create a proper NPM module (don’t forget to mention me for being your inspiration ;)
The middleware is very simple. It defines a Bad Request Error that will be thrown if the incoming request is malformed, and what it does, it simply validate the request against a given schema (a bit later on that) and assigns the validated object (essentially this is req.body but with type conversions and modifiers applied, more on this in joi API documentation) to the req.schema (not the best name) so it can be accessed later on inside the API end point (or any next middleware in the chain).
Now lets talk about the schema and how to actually use this. Schema is just a JSON with joi rules. Lets say we have 2 API end points: One for user creation and one for user update. We define then 2 schema:
module.exports = {
create: Joi.object().keys({
username: Joi.string().min(3).required(),
password: Joi.string().min(3).required(),
email: Joi.string().email().required(),
}),
update: Joi.object().keys({
username: Joi.string().min(3).optional(),
}),
};
In order to create a new user we need to provide 3 required keys: username (at least 3 characters), password (at least 3 characters) and email (should be a real email). And in order to update a user we can just provide an optional new username (at least 3 characters) (side note: an empty request in such case is a valid request, it is up to your end point to make sure to do no-op if the request is empty). We then define our API end points like this:
var SchemaValidationMiddleware = require("path/to/schema/middleware"),
UserSchema = require("path/to/schema/user"),
UserController = require("path/to/user/controller");
router.post(
"/users",
SchemaValidationMiddleware.validate(UserSchema.create),
UserController.createNew,
);
router.patch(
"/users/:id",
SchemaValidationMiddleware.validate(UserSchema.update),
UserController.edit,
);
And inside the controller / API end point we can use req.schema to access the validated schema and perform the required operations. And we can be sure that the controller / end point will be executed only if our request meets the schema we defined for it. The UserController.createNew will never be called if one of the keys we defined as required in our schema — missing, if they are mistyped, if there are keys that we are not expecting and etc.,
This gives the answer to the 2 design keys of a good API that I’ve outlined in the beginning:
- Controllers / end points only manipulates the request and provide a response. They are not used to validate the request. We can be sure that our controllers / end points will never be called with an invalid request.
- By looking at our schema/user.js we can immediately understand how our request should look. We also can give those files to our customers / put them in our API documentation.
You can extend this idea and provide validation for responses as well. By writing a joi schema for the response and adding another middleware after your actual API end point that will validate the response you want to send against a response schema, so this can help you to avoid cases where developers might mistype a response key for examples usrename instead of username. And also by having request and response schema — your API is essentially self documented.
Hope this helped you to become a better JavaScript Jedi! Please share and like! Leave a comment or hit me on Twitter if you have any questions.