This content originally appeared on JavaScript January and was authored by Emily Freeman
Many thanks to Taylor McGann for this article.
Sometimes in JavaScript you need to validate some data to determine whether or not code should continue down a given path. When building RESTful APIs this is often handled by controllers accepting web requests. Usually, frameworks will give you powerful functions or middlwares to validate incoming request data and return a helpful HTTP status code and body. Other times, your best bet is to reach for a well-designed JS schema validation library like Joi, Ajv, or Yup.
However, sometimes you need to validate data outside of a controller, or you don't want to introduce a new library, or you want something more lightweight, say for a quick prototype. In these cases knowing some simple patterns can help you write more effective data validation code. You'll notice that many frameworks and libraries implement similar patterns and functionality based on the underlying principles.
The code examples below will reference the same mock data structure throughout:
const data = { name: 'Taylor McGann', phone: '888-888-8888', }
I kept the mock data simple so it would be understandable, though I admit it's unrealistic and the kinds of validation logic you'll see in the examples below with respect to this data are by no means recommended.
Option A: Boolean Pattern
// module: dataUtils.js export function validateData(data) { // Make sure data has name and phone--no falsy values like undefined, null or empty string--and that the phone includes exactly 10 digits return !!data.name && !!data.phone && data.phone.replace(/\D/g, '').length === 10) } // module: service.js import { validateData } from './dataUtils' // ...get data from somewhere and then validate it... if (!validateData(data)) { // take a different code path since the data failed validation } // data passed validation; proceed with business logic
The Boolean Pattern is all about simplicity and speed of implementation. It's often useful when:
- You only care about the validity of the data but not why it's valid or invalid.
- You want to hide information about the validity (e.g. for security reasons you don't want to expose more information than is necessary).
One of the biggest downsides to this approach that I've found is that I almost always want to know more about why the data was invalid when validation fails so that I can communicate that information to some kind of stakeholder or system (e.g. a user, a web or mobile app, logs, etc.).
Option B: Exception Pattern
// module: dataUtils.js export function validateData(data) { if (!data.name) { throw new Error('Missing required data: name') } if (!data.phone) { throw new Error('Missing required data: phone') } if (data.phone.replace(/\D/g, '').length !== 10) { throw new Error('Phone number must include exactly 10 digits') } } // module: service.js import { validateData } from './dataUtils' // ...get data from somewhere and then validate it... validateData(data) // data passed validation; proceed with business logic
The Exception Pattern is often useful when:
- You want to provide information about the invalid data (e.g. why it's invalid).
- You want to stop validation as soon as invalid data is encountered. This might be because initial errors impact subsequent behavior and/or the determination of subsequent errors.
I'm not particularly in love with this pattern as written because it's hard to tell what will happen if the data is invalid. In my opinion, the absence of a return value reduces code readability especially for engineers unfamiliar with the code. At first glance, you could probably assume that if the data is valid nothing will happen, but if not, then what? Will it throw an error? Are there different kinds of errors? It's not immediately obvious. You have to look at the function body of validateData
to know what to expect.
You could improve the readability and make the intent of the code more obvious by adding a try-catch block:
// module: service.js import { validateData } from './dataUtils' // ...get data from somewhere and then validate it... try { validateData(data) } catch (error) { // rethrow error || throw a new error || handle error depending on type of error or error message } // data passed validation; proceed with business logic
Now you know to expect an error at some point, but is it due to invalid data or something else? Also, if the code simply rethrows the error to handle it in another module, that's a lot of extra code to write just to show that you expect to handle an error. If the code handles the error in catch block, perhaps it's not so bad.
There are other improvements you could make, like using custom error objects which could make error type checking easier. Or you could add unique error codes and check those.
In my opinion, the try-catch approach for data validation feels a bit syntactically verbose, especially if you want to distinguish between error messages or error types.
Option C: Validation Error Pattern
// module: ValidationError.js class ValidationError { static ERROR_CODE_NAME_MISSING = 4100 static ERROR_CODE_PHONE_MISSING = 4200 static ERROR_CODE_PHONE_LENGTH_INVALID = 4201 static defaultErrorMessageMap = { [ValidationError.ERROR_CODE_NAME_MISSING]: 'Missing required data: name', [ValidationError.ERROR_CODE_PHONE_MISSING]: 'Missing required data: phone', [ValidationError.ERROR_CODE_PHONE_LENGTH_INVALID]: 'Phone number must include exactly 10 digits', } constructor(errorCode, errorData, errorMessage = ValidationError.defaultErrorMessageMap[errorCode]) { this.errorCode = errorCode this.errorData = errorData this.errorMessage = errorMessage } } export default ValidationError // module: dataUtils.js import ValidationError from './ValidationError' export function validateData(data) { const errors = [] if (!data.name) { errors.push(new ValidationError(ValidationError.ERROR_CODE_NAME_MISSING, data.name)) } if (!data.phone) { errors.push(new ValidationError(ValidationError.ERROR_CODE_PHONE_MISSING, data.phone)) } if (data.phone.replace(/\D/g, '').length !== 10) { errors.push(new ValidationError(ValidationError.ERROR_CODE_PHONE_LENGTH_INVALID, data.phone)) } return errors } // module: service.js import { validateData } from './dataUtils' // ...get data from somewhere and then validate it... const errors = validateData(data) if (errors.length) { // return errors || throw exception with errors || reconcile invalid data based on error types } // data passed validation; proceed with business logic
The Validation Error Pattern takes some of the advantages of the Exception Pattern but without throwing actual exceptions. This pattern is often useful when:
- You want to provide information about the invalid data (e.g. why it's invalid).
- You don't want to "short circuit" the data validation process after the first error because you want to notify a user or system of all possible errors to reduce the number of required attempts
- You want to process any data that will succeed (i.e. the process is not an "all or nothing" batch transaction). Examples of this are beyond the scope of this article since it relates to more complicated data processing with multiple stages.
As written here, this pattern is considerably heavier. It doesn't have to be though--you could use numbers, strings or object literals instead of classes. You could also modify it to return after the first error.
Option D: Validation Result Pattern
// module: ValidationResult.js import ValidationError from './ValidationError' class ValidationResult { errors = [] constructor(result, commonData) { this.result = result this.commonData = commonData } addError(errorCode, errorData) { this.errors.push(new ValidationError(errorCode, errorData)) return this.errors } hasErrors() { return this.errors.length > 0 } } export default ValidationResult // module: dataUtils.js import ValidationError from './ValidationError' import ValidationResult from './ValidationResult' export function validateData(data) { const result = new ValidationResult() if (!data.name) { result.addError(ValidationError.ERROR_CODE_NAME_MISSING, data.name) } if (!data.phone) { result.addError(ValidationError.ERROR_CODE_PHONE_MISSING, data.phone)) } if (data.phone.replace(/\D/g, '').length !== 10) { result.addError(ValidationError.ERROR_CODE_PHONE_LENGTH_INVALID, data.phone) } return result } // module: service.js import { validateData } from './dataUtils' // ...get data from somewhere and then validate it... const result = validateData(data) if (result.hasErrors()) { // return result || return errors || throw exception with errors || reconcile invalid data based on error types } // data passed validation; proceed with business logic
The Validation Result Pattern is useful for the same reasons as the Validation Error Pattern and also when:
- There is data that needs to be communicated with a defined result data structure regardless of errors (e.g. some data has been processed while other data failed validation)
- There is data that is common to all errors and therefore can be captured in a single place (i.e. the
ValidationResult
)
Probably the biggest downside to this pattern is how much "heavier" it is. It's far more complicated and involved. As mentioned in the introduction, it's always a good idea to use prior art found in frameworks and schema validation libraries where possible.
Conclusion
Hopefully this gives you some good ideas on how you can design and implement basic data validation. There's certainly a lot more possibility and variation beyond the options presented here.
If I find I need to write a data validation function, most of the time I find myself reaching for some form of the Validation Error Pattern. I find it meets most of my needs for checking validity and communicating information.
This content originally appeared on JavaScript January and was authored by Emily Freeman
Emily Freeman | Sciencx (2020-01-30T10:28:00+00:00) JavaScript Data Validation Patterns. Retrieved from https://www.scien.cx/2020/01/30/javascript-data-validation-patterns/
Please log in to upload a file.
There are no updates yet.
Click the Upload button above to add an update.