Beautiful Node: Building an API endpoint with Express, Mongoose, Validation and Promises
To be honest, when a new JavaScript language feature lands in Node.js, I am always convinced that I don’t need this and and won’t use it for the time being. I mean — I’m doing JavaScript for oh-so-long now and was totally fine before es5, es6 or es-whatever.
But then you know — using always the same syntax gets boring and I start to play around with it and somehow it sneaks into my projects and soon I find myself using it on a daily basis. This happened to me before with es6 classes, then with promises and lately with async/await and I have to confess that my code is much easier to read now. This does not mean that promises and async/await are a fool-proof drop-in for callbacks and that I didn’t run into totally new problems while using them.
In this blog post I will show you how to create a beautiful API endpoint utilizing Mongoose validation and async/await.
Enter Promiseland
As I said — Promises and async/await aren’t just another way to express callbacks and async/await isn’t just another syntax for promises and as such understanding them and knowing how the modules you are using support them is key. Take Mongoose as an example. As the docs explain, some operations will return a ‘fully fledged’ promise while others won’t unless you use the exec() function on them. Confused? I am still, sometimes. As I said — promises aren’t just syntactic sugar (as classes are).
How to validate data
Making sure that only valid data ends up in your database is key if you want to keep your sanity — especially when dealing with user-provided inputs. The question is: Where should you put all this logic?
Doing everything in your routes (don’t!)
If you are using any framework like express, you will most probably define a route and receive user-data either via parameter (req.params.x) or from forms through the request body (req.body). Within your route handler, you could now do your sanity checks using some if-statements and save() the data to the database.
Let’s see a simple example. We want to let users create a project by providing a GitHub URL to it. To validate the URL, we use a module called is-github-url.
As mentioned, I’m a big async/await fan now and I’m using it here already to wait for the result of my save() operation.
While handling everything within the route works, it also means that your routes, your data validation and your database logic are very tightly coupled. You will end up with database statements spread throughout your code. Especially if you want to return data from the database through a query, your routes will get bloated and — most importantly - you will definitely start duplicating logic and duplicating logic is a bad thing.
You might also wonder, why I’m using a try/catch block here. Read on to learn why.
Abstraction to the rescue: Using service modules / classes (maybe)
Any time you find yourself repeating yourself you should think of abstracting this logic away. I often use service classes for that. They provide all data access methods I need. Usually I use one service per model.
As you see in my example, I moved all the logic into the service class. This means that I can now consistently create a new entry from anywhere.
Dealing with Rejection
The validation is now throwing an error if something goes wrong. Wait — weren’t we told that you should never throw in asynchronous functions? This was true for a long time when we used error-first callbacks to pass back errors. Now with promises and async/await you could either return Promise.reject(‘Some error’) or throw. Both are equal. Losing error-first callbacks creates a lot of new challenges — mostly because critical system level errors that should abort the Node process throw as well — this means they use the same ‘channel’ as ‘userland’ errors like the validation violation in the sample. It’s now easy to unintentionally swallow severe errors. Read more about that in this great article by Eran Hammer.
Using service classes is a good level of abstraction and it’s legit to do all validation there. In larger projects, though it happened to me that I forgot to consistently validate my data. Think of an update method and a developer happens to forget the validator.There is no easy way to enforce that within your service classes. This is where Mongoose validation comes into play.
Using Mongoose Validation (do that)
With Mongoose validation, you can define your validators directly in your model and Mongoose ensures that only validated data ends up in the database (there is a caveat though: by default updates aren’t validated — I’ll come to that later).
As you can see, I defined a set of rules on the githubUrl property.
I want it to be trimmed and lowercased (this happens before the validation takes place) and I defined a validator that will simply execute isGithubUrl() with the data provided. If the provided data is invalid, the save() method will throw an error. I can catch that error within my route and return it to the user.
In this sample, I am doing a basic test to check if it’s a validation error. In any other case (e.g. the database is offline) I will rethrow and deal with it on a higher level.
By moving all validation into the model, the code I have to touch on a daily basis is much cleaner now and I can be confident that it’s centrally validated before written to the database.
What about Updates?
I mentioned before that, unfortunately, validating on update has some caveats. If you load a record, change it and save it again, everything works as showed before. The problem starts, if you run an update query (e.g. via(Model.update()) against the database. In this case you have to pass ‘{ runValidators: true }’ as an option and there are a few things to consider with more complex validation functions that need access to this. While this is unfortunate it’s not unsolvable and the Mongoose docs cover that topic in depth.
What about Testing?
When I started that post, I actually wanted to cover testing as well and it‘s quite long already. So stay tuned — I have a working test setup with mocha and chai that I can’t wait to share.
Shameless Self Promotion
If you like my blog posts, maybe you also like my online courses where I‘m using end-to-end real-world examples to cover best practices in much more detail :)
Questions / Feedback / Suggestions?
Please use the comment section for feedback, questions or if you found an error.