We did so far an amazing job about validation, let's move it to the next level by adding some configurations to our validation process.
Configurations
Open src/config
and create validation.ts
file.
// src/config/validation.ts
const validationConfigurations = {
rules: {},
};
export default validationConfigurations;
Just an empty object for now, now let's update the index file of the configuration to inject it in the general configurations.
// src/config/index.ts
import config from "@mongez/config";
import { databaseConfigurations } from "config/database";
import validationConfigurations from "./validation";
config.set({
database: databaseConfigurations,
validation: validationConfigurations,
});
Now can access the configurations from anywhere in the application.
Rules List
Remember the rulesTypes
that we added in RulesList
class? we'll lets remove it and use the configurations instead.
// src/validation/rules-list.ts
import config from "@mongez/config";
export default class RulesList {
/**
* Errors list
*/
protected errorsList: any = [];
/**
* Constructor
*/
public constructor(
protected readonly input: string,
protected readonly value: any,
protected readonly rules: any,
) {
//
}
/**
* Validate the rules
*/
public async validate() {
for (const ruleName of this.rules) {
// the rule will be in the rules section under the validation object
const RuleClass = config.get(`validation.rules.${ruleName}`);
const rule = new RuleClass(this.input, this.value);
await rule.validate();
if (rule.fails()) {
this.errorsList.push(rule.error());
}
}
}
/**
* Check if validator fails
*/
public fails() {
return this.errorsList.length > 0;
}
/**
* Check if validator passes
*/
public passes() {
return this.errorsList.length === 0;
}
/**
* Get errors list
*/
public errors() {
return {
input: this.input,
errors: this.errorsList,
};
}
}
What we updated here we removed rulesTypes
and we replaced it with config.get('validation.rules.${ruleName}')
which will get the rule from the configurations.
Now let's update our rules in the configurations file
// src/config/validation.ts
import { RequiredRule, StringRule } from "core/validator";
const validationConfigurations = {
rules: {
[RequiredRule.ruleName]: RequiredRule,
[StringRule.ruleName]: StringRule,
},
};
export default validationConfigurations;
We imported the StringRule
and RequiredRule
which we didn't export it yet from validator
folder, so let's do it.
Remember the ruleName static property? that's why we created it to use it here with more fancy code :)
We need first to move the validator class to a separate file so we can make the index file more readable.
Rename index.ts
to validator.ts
and now create index.ts
file.
// src/core/validator/index.ts
export { default as RulesList } from "./rules-list";
export { default as RequiredRule } from "./rules/required";
export { default as Rule } from "./rules/rule";
export { default as StringRule } from "./rules/string";
export { default as Validator } from "./validator";
We exported the Validator class, Rules List, Rule class, RequiredRule and StringRule classes.
So when we create a new rule, don't forget to export it from the index file.
Now update the request.ts
file and update the import from validator
to core/validator
.
// src/core/http/request.ts
import { Validator } from "core/validator";
// ...
Now give it a try, it should be working as expected.
Stop on first failure
We can add a new configuration to stop the validation process on the first failure so we don't need to validate all the rules if the first one fails.
// src/config/validation.ts
const validationConfigurations = {
rules: {
[RequiredRule.ruleName]: RequiredRule,
[StringRule.ruleName]: StringRule,
},
stopOnFirstFailure: true,
};
Now let's implement it.
Go to rules-list.ts
file and update the validate
method.
/**
* Validate the rules
*/
public async validate() {
for (const ruleName of this.rules) {
// the rule will be in the rules section under the validation object
const RuleClass = config.get(`validation.rules.${ruleName}`);
const rule = new RuleClass(this.input, this.value);
await rule.validate();
if (rule.fails()) {
this.errorsList.push(rule.error());
// stop the loop after it fails
if (config.get("validation.stopOnFirstFailure")) {
break;
}
}
}
}
Check the code inside the if
statement, we check if the stopOnFirstFailure
is true, if it is true we break the loop.
Return Error Strategy
We can add another configuration to return the error strategy, we can return the first error or all errors.
// src/config/validation.ts
const validationConfigurations = {
rules: {
[RequiredRule.ruleName]: RequiredRule,
[StringRule.ruleName]: StringRule,
},
stopOnFirstFailure: true,
returnErrorStrategy: "first", // first or all
};
Now we added a new configuration called returnErrorStrategy
and we set it to first
.
So we can choose between first
and all
to determine how we can return the error.
Let implement it.
// src/validation/rules-list.ts
/**
* Get errors list
*/
public errors() {
if (config.get("validation.returnErrorStrategy") === "first") {
return {
input: this.input,
errors: this.errorsList[0],
};
}
return {
input: this.input,
errors: this.errorsList,
};
}
Error Keys
We can also control the error keys
that will be return to response.
// src/config/validation.ts
const validationConfigurations = {
rules: {
[RequiredRule.ruleName]: RequiredRule,
[StringRule.ruleName]: StringRule,
},
stopOnFirstFailure: true,
returnErrorStrategy: "first", // first or all
keys: {
response: 'errors', // the key that will be used to return the entire errors
inputKey: "input",
inputErrors: "errors", // for all errors
inputError: "error", // for first error
},
};
We added a new configuration called keys
and we set the response
key to errors
which will be used to return the entire errors.
The inputKey
will be used to return the input key.
The inputErrors
will be used to return the errors list if we choose all
strategy.
The inputError
will be used to return the first error if we choose first
strategy.
Now let's implement it.
// src/core/validator/rules-list.ts
/**
* Get errors list
*/
public errors() {
const responseKey = config.get("validation.keys.response");
const inputKey = config.get("validation.keys.inputKey");
const inputErrorsKey = config.get("validation.keys.inputErrors");
const inputErrorKey = config.get("validation.keys.inputError");
if (config.get("validation.returnErrorStrategy") === "first") {
return {
[responseKey]: {
[inputKey]: this.input,
[inputErrorKey]: this.errorsList[0],
},
};
}
return {
[responseKey]: {
[inputKey]: this.input,
[inputErrorsKey]: this.errorsList,
},
};
}
Awesome, right?
Now let's update request.ts
file and update the validate
method.
// src/core/http/request.ts
/**
* Execute the request
*/
public async execute() {
if (this.handler.validation) {
const validator = new Validator(this, this.handler.validation.rules);
await validator.scan(); // start scanning the rules
if (validator.fails()) {
const responseKey = config.get("validation.keys.response");
return this.response.status(422).send({
[responseKey]: validator.errors(),
});
}
}
return await this.handler(this, this.response);
}
Configurations defaults
This will work perfectly, but if we did not forget to import the configurations, what if we forgot to add the configurations?
In that case, we must add the default configurations, THANKFULLY
config.get
accepts second argument as default value, so let's update all the configurations to use the default value.
// src/core/validator/rules-list.ts
/**
* Get errors list
*/
public errors() {
const responseKey = config.get("validation.keys.response", "errors");
const inputKey = config.get("validation.keys.inputKey", "input");
const inputErrorsKey = config.get("validation.keys.inputErrors", "errors");
const inputErrorKey = config.get("validation.keys.inputError", "error");
if (config.get("validation.returnErrorStrategy", "first") === "first") {
return {
[responseKey]: {
[inputKey]: this.input,
[inputErrorKey]: this.errorsList[0],
},
};
}
return {
[responseKey]: {
[inputKey]: this.input,
[inputErrorsKey]: this.errorsList,
},
};
}
// src/core/http/request.ts
/**
* Execute the request
*/
public async execute() {
if (this.handler.validation) {
const validator = new Validator(this, this.handler.validation.rules);
await validator.scan(); // start scanning the rules
if (validator.fails()) {
const responseKey = config.get("validation.keys.response", "errors");
return this.response.status(422).send({
[responseKey]: validator.errors(),
});
}
}
return await this.handler(this, this.response);
}
Response Status Error Configuration
That's my bad, we'll use status 400, i wrote it in the previous articles as 422, which is another status code.
We can also add a configuration to control the response status code when the validation fails which we set to to 400 which is a bad request
// src/config/validation.ts
const validationConfigurations = {
rules: {
[RequiredRule.ruleName]: RequiredRule,
[StringRule.ruleName]: StringRule,
},
stopOnFirstFailure: true,
returnErrorStrategy: "first", // first or all
keys: {
response: "errors", // the key that will be used to return the entire errors
inputKey: "input",
inputErrors: "errors", // for all errors
inputError: "error", // for first error
},
responseStatus: 400,
};
Now let's implement it.
// src/core/http/request.ts
/**
* Execute the request
*/
public async execute() {
if (this.handler.validation) {
const validator = new Validator(this, this.handler.validation.rules);
await validator.scan(); // start scanning the rules
if (validator.fails()) {
const responseKey = config.get("validation.keys.response", "errors");
return this.response
.status(config.get("validation.responseStatus", 400))
.send({
[responseKey]: validator.errors(),
});
}
}
return await this.handler(this, this.response);
}
And that's it!
We have now a powerful validation class with a lot of configurations.
🎨 Project Repository
You can find the latest updates of this project on Github
😍 Join our community
Join our community on Discord to get help and support (Node Js 2023 Channel).
🎞️ Video Course (Arabic Voice)
If you want to learn this course in video format, you can find it on Youtube, the course is in Arabic language.
💰 Bonus Content 💰
You may have a look at these articles, it will definitely boost your knowledge and productivity.
General Topics
- Event Driven Architecture: A Practical Guide in Javascript
- Best Practices For Case Styles: Camel, Pascal, Snake, and Kebab Case In Node And Javascript
- After 6 years of practicing MongoDB, Here are my thoughts on MongoDB vs MySQL
Packages & Libraries
- Collections: Your ultimate Javascript Arrays Manager
- Supportive Is: an elegant utility to check types of values in JavaScript
- Localization: An agnostic i18n package to manage localization in your project
React Js Packages
Courses (Articles)
Top comments (0)