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.
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:
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:
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.