28-Nodejs Course 2023: Validation Part VI: Custom Validation
Hasan Zohdy
Posted on November 2, 2022
We have made such an amazing progress in validation, but there is still more to be done.
Configurations Types
We have created configurations list successfully, but let's give it a more fancy touch, let's define a type for it.
Create types.ts
file in src/core/validator
directory.
// src/core/validator/types.ts
export type ValidationConfigurations = {
/**
* Whether to stop validator after first failed rule
*
* @default true
*/
stopOnFirstFailure?: boolean;
}
We added only one property for now to see how i'm writing it.
We first write documentation for it, if we set a default value then we write it in the documentation, and then we define the type.
Now let's add the rest of the configurations.
// src/core/validator/types.ts
import Rule from "./rules/rule";
export type ValidationConfigurations = {
/**
* Whether to stop validator after first failed rule
*
* @default true
*/
stopOnFirstFailure?: boolean;
/**
* Return Error Strategy
* If strategy is `first` then it will return a single error in string from the rule errors list
* If strategy is `all` then it will return an array of string that contains all errors.
*
* The `all` strategy will be affected as well with `stopOnFirstFailure` if it is set to `true`
* and strategy is set to `all` it will always return an array with one value
*
* @default first
*/
returnErrorStrategy?: "first" | "all";
/**
* Response status code
*
* @default 400
*/
responseStatus?: number;
/**
* Validation keys
*/
keys?: {
/**
* Response error key that will wrap the entire errors
*
* @default errors
*/
response?: string;
/**
* Input key name
*
* @default input
*/
inputKey?: string;
/**
* Single Error key (when strategy is set to first)
*
* @default error
*/
inputError?: string;
/**
* Multiple Errors key (when strategy is set to all)
*
* @default errors
*/
inputErrors?: string;
};
/**
* Rules list that will be used in the validation process
*/
rules?: Record<string, typeof Rule>;
};
All types are pretty much self explained, except the rules
one, we used the Record
type which means we're telling Typescript the rules
will be an object, its key will be string
and its value will be typeof Rule
class.
Now let's export it in the validator index then import it in the validator.ts
configuration file.
// src/core/validator/index.ts
export { default as RequiredRule } from "./rules/required";
export { default as Rule } from "./rules/rule";
export { default as StringRule } from "./rules/string";
export * from "./types";
export { default as Validator } from "./validator";
// src/config/validation.ts
import {
RequiredRule,
StringRule,
ValidationConfigurations,
} from "core/validator";
const validationConfigurations: ValidationConfigurations = {
stopOnFirstFailure: true,
returnErrorStrategy: "first",
responseStatus: 400,
rules: {
[RequiredRule.ruleName]: RequiredRule,
[StringRule.ruleName]: StringRule,
},
keys: {
response: "messages",
inputKey: "key",
inputError: "error",
inputErrors: "errors",
},
};
export default validationConfigurations;
The amazing thing about typescript, is that now you can use the auto complete feature in your vscode.
Try to remove all configurations and inside the object press ctrl + space
and you will see all the configurations that you can use.
Missing Rule Handler
As we're going big, we need to make sure that our code is working as expected, what if we tried to add a rule that does not exist in the rules
list?
In that case the application will definitely crash, so let's add a handler for that.
// src/core/validator/rules-list.ts
import chalk from "chalk";
/**
* Validate the rules
*/
public async validate() {
for (const ruleName of this.rules) {
const RuleClass = config.get(`validation.rules.${ruleName}`);
if (!RuleClass) {
throw new Error(
chalk.bold(
`Missing Rule: ${chalk.redBright(
ruleName,
)} rule is not listed in ${chalk.cyan(
"validation.rules",
)} configurations,`,
),
);
}
const rule = new RuleClass(this.input, this.value);
await rule.validate();
if (rule.fails()) {
this.errorsList.push(rule.error());
if (config.get("validation.stopOnFirstFailure", true)) {
break;
}
}
}
}
We added a check here to see if the rule exists in the rules
list, if not then we throw an error.
But we need to catch that error to be displayed as you won't see any error in the console.
Open the request class to wrap the validator in a try catch block.
// src/core/http/request.ts
/**
* Execute the request
*/
public async execute() {
if (this.handler.validation) {
const validator = new Validator(this, this.handler.validation.rules);
try {
await validator.scan(); // start scanning the rules
} catch (error) {
// this is needed to catch the error thrown by the missing rule handler
console.log(error);
}
if (validator.fails()) {
const responseErrorsKey = config.get(
"validation.keys.response",
"errors",
);
const responseStatus = config.get("validation.responseStatus", 400);
return this.response.status(responseStatus).send({
[responseErrorsKey]: validator.errors(),
});
}
}
return await this.handler(this, this.response);
}
Now go to create-user
and add email
rule that does not exist in the validation.rules
list.
Mark
stopOnFirstFailure
asfalse
in thevalidation.ts
file to see the difference.
You should now see something like this in your terminal:
Custom Validation
So we're so far now good with validation, but sometimes rules are not enough, we might need to do some custom validations on the requests besides the rules.
That's where we'll introduce a new validate
method in the handler validation
object.
Validate Method
The handler (controller) has a validation
property, which contains the rules
property to validate inputs.
We will also add the ability to add a validate
method to the validation
object so we can do any custom validation.
How it works
The validate
method will receive the request and the response as parameters, and it will be executed after the rules validation.
If the validate
method returns a value, this will be returned, otherwise the handler will be executed.
Also we'll make it async
function so we can perform any async operation inside it.
Let's jump into the code.
// src/core/http/request.ts
/**
* Execute the request
*/
public async execute() {
if (this.handler.validation) {
// rules validation
if (this.handler.validation.rules) {
const validator = new Validator(this, this.handler.validation.rules);
try {
await validator.scan(); // start scanning the rules
} catch (error) {
console.log(error);
}
if (validator.fails()) {
const responseErrorsKey = config.get(
"validation.keys.response",
"errors",
);
const responseStatus = config.get("validation.responseStatus", 400);
return this.response.status(responseStatus).send({
[responseErrorsKey]: validator.errors(),
});
}
}
// custom validation
if (this.handler.validation.validate) {
const result = await this.handler.validation.validate(
this,
this.response,
);
if (result) {
return result;
}
}
}
return await this.handler(this, this.response);
}
We wrapped the rules
validation with a new if statement, to see if the request has rules
property.
If the rules property exists, then execute and validate it first, if failed, then stop executing the handler and the custom validator as well.
Now let's give it a try, open create-user
handler and add the validate
method.
// src/app/users/controllers/create-user.ts
import { Request } from "core/http/request";
export default async function createUser(request: Request) {
const { name, email } = request.body;
return {
name,
};
}
createUser.validation = {
// rules: {
// name: ["required", "string"],
// email: ["required", "string"],
// },
validate: async () => {
return {
error: "Bad Request",
};
},
};
We commented the rules only for testing purposes, and added a validate
method that returns an object.
If the validate returns any value, then it will return that value, otherwise it will execute the handler.
Conclusion
We've reached the end of this article, we've added the configurations types, we also wrapped the rules validation in a try catch block to catch any errors thrown by the rules.
Also we added a custom validation method to the handler, so we can do any custom validation on the request.
🎨 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)
Posted on November 2, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.