Data validation is a pain. Not only is it hard to do it right, but it can also be difficult to implement without making a mess. When trying to validate data before saving it, it’s easy to pollute methods and violate many programming best practices.
For all those reasons, a much better way would be to handle validation via a validator service. In programming, a service is a unit that externalizes business logic from entities.
Using services is a great way to keep a clean codebase by breaking down responsibilities. A validator service will allow us to avoid having to explicitly validate data ourselves in our methods. Instead, we’ll create a reusable unit we can rely on to build the foundations of our program.
The world before using a validator service
Let’s say we’re building a simple model. Until now, when we wanted to validate data before setting it, our model’s setter methods looked something like this:
Meh. I don’t know about you, but I dislike having the entire content of a method wrapped into an
if statement. If the whole purpose of my method is conditional, it makes me feel like I skipped a step. Like there’s something I need to take care of beforehand. Plus, do you imagine what we’ll need to do if we have to add rules?
Gross. Imagine if we wanted to set several values, like all the properties of a single object, in the same method? I don’t even want to figure out what it would look like.
And in fact, there’s much more than one problem to this code structure:
- It’s hard to read. As seen above, we can quickly end up with many successive or imbricated
ifstatements, bloated with complex validation rules. To change it you’d have to spend time understanding what’s going on, then perform your edit, while making sure you’re not breaking anything.
- It’s repetitive. Chances are all your setter methods will have one or more
ifstatements that pretty much do the same thing. This goes against the fundamental DRY principle, which states “every piece of knowledge must have a single, unambiguous, authoritative representation within a system”. The goal is for us to keep every change to this knowledge in sync across the entire program. If you have repetitive code to check if a value is not empty, for example by doing
value !== '', and then want to reinforce it by adding
&& value !== null && typeof value !== 'undefined', you’ll have to find every occurrence of your former code and update it. This is tedious, time-consuming, has low added-value, and prone to bugs.
- It violates SRP. The single responsibility principle states “a class should have only one reason to change”. In the above example, our method does two things: it validates data and it assigns it if the test passes. We could even say it does more than that because it stores validation rules for the model’s
nameproperty (making it impossible to use elsewhere without duplicating code). The point is, the method has more than one responsibility, and this makes it more difficult to test, reuse, read and refactor.
- It couples logic and data. Right now every time we need to change the validation rules for this method, we’ll have to touch the
ifstatement. As a direct consequence of violating SRP, the method couples two things that should be separate: a logic mechanism that should never change, and variable data.
If we want to build a clear, solid and scalable program, this is everything we want to avoid. What we want is something with a validation mechanism that’s set once in a generic way so we can reuse it. We also want to store our validation rules separately, somewhere where it makes more sense, and in a way that’s easily editable. Finally, we want validation to be “invisible”: it must be done under the hood and never appear explicitly in our setter methods.
Starting with what you want
When I’m tackling a new problem and I don’t know where to start, my method of choice is reverse engineering. I start with what I want my final code to be and I backward build to make it work.
Here’s how we’d ideally want to setup the model:
This would be perfect. It doesn’t work yet, but this is what we’d want our validator to deal with in the end. Instead of assigning values directly to a variable, we decide every property is an object with at least two sub-properties: its actual value, and an array of its validation rules. This is legible, it has a standard format and we can easily add, change or remove rules. We don’t have to worry about the actual validation system that will handle them.
From here, let’s start thinking about how we can build our
Validator object. We want to validate data from an array of strings. So, for every rule, we need a dedicated method that uses the exact same name.
Every validation method follows the same model: it takes a value, makes sure it’s valid and returns a boolean. Instead of using messy
if statements directly in setter methods, we have a much cleaner way to validate data:
Now, this is nice but we don’t actually want to do it by hand. We want to list out rules in the model and for them to be automatically validated. So for this, we need a method:
Don’t freak out, I’ll explain 😁 What we want is for our value to be checked against a set of rules. This could be one rule or a thousand, which is why it’s ideal to have rules listed in an array. If at least one rule doesn’t pass we need to return
false, else we’ll return
Our method takes two arguments: a value to test, and a set of rules as an array. Each rule must be the exact name of the validator’s method that needs to be called, as a
Array.prototype.every(). As explained on MDN, it “tests whether all elements in the array pass the test implemented by the provided function”. We use it to call every rule one after the other, and we pass it the value we want to test. Since
every() needs every (duh!) test to return
true to itself return
true, we can simply return it.
And what about this
This line is what’s going to call the appropriate validator method for each rule. Let’s break this down:
- We defined the
selfvariable two lines prior, and it refers to
thisin the context of the
Validatorbut the scope changes within the callback function of
every(). Because it’s using the function invocation pattern, using
thiswould refer to
Validator. To circumvent that, we stored the value of
thisin a variable while we still were in the desired context. Now we can use the variable within the callback and it will refer to the
ruleis the current element being iterated on by
ruleas the key, within brackets, to call the right method.
valueis the argument being tested. It’s wrapped in parentheses because the member we’re accessing is a function. We’re using an invocation operator to invoke it and we pass
valueas an argument.
At each loop, we iterate over a new rule from the
rules array and we use it to call the name-matching function. So with the
['isString', 'isNotEmpty'] array, the loop is calling
Validator.prototype.isNotEmpty(), and passing them the
A step further
We have a good, working system. We can use our validator either to test a value with one rule or go with the
validate() method for a whole set of rules.
Remember the checklist we defined earlier as the ideal system we’d want to build? Let’s get back to it and see if we fulfilled the contract. We wanted:
- ✅ something with a validation mechanism that’s set once in a generic way and can be reused
- ✅ store our validation rules separately and in a way that’s easily editable
- validation to be “invisible” in our setter methods
We may have a neat way to validate data, but we still need to wrap it inside an
if statement to make it work.
It’s much better than before but still not ideal. Sure, if we need to change the validation rules we won’t have to touch that
if statement. Yet, we’re still explicitly doing two different things in our method. This means we’ll need to do it in every new setter method, which is repetitive and not as legible as it could be.
Let’s try to think about a method that would do it all for us: check if the validation passes, and set the data if it does. We’d only have to pass the value and the object property to assign it to, and it would handle all the heavy lifting for us.
Here we have a dedicated method
Model.prototype.set(). It takes two arguments: the value to test and the key to assign it to. The method will call the validator’s
validate() method and pass the value and the key’s
validator property. If the test passes, the value will be assigned to the key’s
value property and return
true. Else, it will return
Now instead of having an
if statement in our
setName() method, we can simply call the
set() method and rely on it to perform the assignment if the tests pass. We can look at it as a “safe” and generic setter method.
That’s it: every need is met 🎉 The code is clean, it’s a breeze to read, and there’s no sign of explicit validation. By using a service, we have not only automated recurring logic and made it easier to maintain, but we have freed our model from clutter. Unless we have to add new validation methods, there’s no reason for us or any newcomer to even open the validator file. All we have to do is use it. We don’t really have to understand the internals to make it work, but rather rely on it as a black box. This makes the learning curve of the project a lot more gentle.
Another great thing is how we created a clean bridge between the generic validator and our specific model with
Model.prototype.set(). If we wanted to use the validator with another model, or any other function, but they had a different way of storing validation rules, we could still do it. All we’d have to do is create another bridge. The model’s
set() method binds the validator to itself based on how it’s built, but the validator remains generic. We haven’t specialized our validator, and we have a clean, reusable way to validate then set data in the model.