Код валидации быстро превращается в источник технического долга. Особенно это заметно в проектах, где:
if-цепочки;Типичный пример проблемного кода:
function validateUser(data) {
if (!data.email) {
return 'Email обязателен';
}
const emailRegex =
/^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@(([^<>()[\]\\.,;:\s@"]+\.)+[^<>()[\]\\.,;:\s@"]{2,})$/i;
if (!emailRegex.test(data.email)) {
return 'Некорректный email';
}
if (!data.password) {
return 'Пароль обязателен';
}
if (data.password.length < 8) {
return 'Минимум 8 символов';
}
return null;
}
Проблемы такого подхода:
Validator.js позволяет сократить объём кода и стандартизировать проверки.
function isValidEmail(email) {
const regex =
/^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@(([^<>()[\]\\.,;:\s@"]+\.)+[^<>()[\]\\.,;:\s@"]{2,})$/i;
return regex.test(email);
}
Регулярное выражение практически невозможно поддерживать.
Даже небольшое изменение regex может сломать проверку.
Каждый разработчик может использовать собственный regex.
import validator from 'validator';
function isValidEmail(email) {
return validator.isEmail(email);
}
Было:
const regex = /^...$/;
Стало:
validator.isEmail(email);
Логика проверки скрыта внутри библиотеки.
Validator.js учитывает множество edge-case сценариев.
function validatePassword(password) {
if (!password) {
return false;
}
if (password.length < 8) {
return false;
}
if (!/[A-Z]/.test(password)) {
return false;
}
if (!/[0-9]/.test(password)) {
return false;
}
return true;
}
import validator from 'validator';
function validatePassword(password) {
return validator.isStrongPassword(password, {
minLength: 8,
minUppercase: 1,
minNumbers: 1
});
}
Вместо множества условий используется декларативный объект.
minLength: 12
const passwordRules = {
minLength: 10,
minUppercase: 1,
minNumbers: 2,
minSymbols: 1
};
function registerUser(data) {
if (!validator.isEmail(data.email)) {
throw new Error('Invalid email');
}
}
function updateUser(data) {
if (!validator.isEmail(data.email)) {
throw new Error('Invalid email');
}
}
function resetPassword(data) {
if (!validator.isEmail(data.email)) {
throw new Error('Invalid email');
}
}
import validator from 'validator';
function validateEmail(email) {
if (!validator.isEmail(email)) {
throw new Error('Invalid email');
}
}
function registerUser(data) {
validateEmail(data.email);
}
function updateUser(data) {
validateEmail(data.email);
}
function resetPassword(data) {
validateEmail(data.email);
}
validators/
├── emailValidator.js
├── passwordValidator.js
├── userValidator.js
└── index.js
import validator from 'validator';
export function validateEmail(email) {
return validator.isEmail(email);
}
import validator from 'validator';
export function validatePassword(password) {
return validator.isStrongPassword(password);
}
import { validateEmail } from './emailValidator.js';
import { validatePassword } from './passwordValidator.js';
export function validateUser(user) {
return (
validateEmail(user.email) &&
validatePassword(user.password)
);
}
function validateProfile(profile) {
if (profile) {
if (profile.email) {
if (profile.email.length > 5) {
return validator.isEmail(profile.email);
}
}
}
return false;
}
function validateProfile(profile) {
if (!profile?.email) {
return false;
}
return validator.isEmail(profile.email);
}
function validateProfile(profile) {
return validator.isEmail(profile?.email || '');
}
function validateUrl(url) {
return /^https?:\/\/.+/.test(url);
}
import validator from 'validator';
function validateUrl(url) {
return validator.isURL(url);
}
validator.isURL(url, {
protocols: ['https'],
require_protocol: true
});
function sanitizeInput(text) {
return text.trim().toLowerCase();
}
import validator from 'validator';
function sanitizeInput(text) {
return validator.trim(
validator.escape(
validator.toLowerCase(text)
)
);
}
function normalizeEmail(email) {
email = email.trim();
email = email.toLowerCase();
return email;
}
function normalizeEmail(email) {
return validator.normalizeEmail(email);
}
function validateForm(data) {
const errors = {};
if (!data.email) {
errors.email = 'Required';
} else if (!validator.isEmail(data.email)) {
errors.email = 'Invalid';
}
if (!data.password) {
errors.password = 'Required';
} else if (data.password.length < 8) {
errors.password = 'Too short';
}
return errors;
}
function validateEmail(email) {
if (!email) {
return 'Required';
}
if (!validator.isEmail(email)) {
return 'Invalid';
}
return null;
}
function validatePassword(password) {
if (!password) {
return 'Required';
}
if (!validator.isStrongPassword(password)) {
return 'Weak password';
}
return null;
}
function validateForm(data) {
return {
email: validateEmail(data.email),
password: validatePassword(data.password)
};
}
function validateUsername(username) {
if (!username) {
return false;
}
if (username.length < 3) {
return false;
}
if (username.length > 20) {
return false;
}
return true;
}
const usernameRules = [
value => !validator.isEmpty(value),
value => validator.isLength(value, {
min: 3,
max: 20
})
];
function validateUsername(username) {
return usernameRules.every(rule => rule(username));
}
const rules = [
{
validate: value => validator.isEmail(value),
message: 'Invalid email'
},
{
validate: value => validator.isLength(value, {
min: 5
}),
message: 'Too short'
}
];
function validate(value) {
const errors = [];
for (const rule of rules) {
if (!rule.validate(value)) {
errors.push(rule.message);
}
}
return errors;
}
app.post('/register', (req, res) => {
const { email } = req.body;
if (!validator.isEmail(email)) {
return res.status(400).json({
error: 'Invalid email'
});
}
res.send('OK');
});
function emailValidationMiddleware(req, res, next) {
if (!validator.isEmail(req.body.email)) {
return res.status(400).json({
error: 'Invalid email'
});
}
next();
}
app.post(
'/register',
emailValidationMiddleware,
controller
);
function createLengthValidator(min, max) {
return value =>
validator.isLength(value, {
min,
max
});
}
const validateUsername =
createLengthValidator(3, 20);
const validateDescription =
createLengthValidator(10, 500);
function validateUser(user) {
if (!validator.isEmail(user.email)) {
return false;
}
if (!validator.isStrongPassword(user.password)) {
return false;
}
if (!validator.isLength(user.username, {
min: 3,
max: 20
})) {
return false;
}
if (!validator.isMobilePhone(user.phone)) {
return false;
}
return true;
}
function validateEmail(email) {
return validator.isEmail(email);
}
function validatePassword(password) {
return validator.isStrongPassword(password);
}
function validateUsername(username) {
return validator.isLength(username, {
min: 3,
max: 20
});
}
function validatePhone(phone) {
return validator.isMobilePhone(phone);
}
function validateUser(user) {
return (
validateEmail(user.email) &&
validatePassword(user.password) &&
validateUsername(user.username) &&
validatePhone(user.phone)
);
}
const schema = {
email: value => validator.isEmail(value),
username: value =>
validator.isLength(value, {
min: 3,
max: 20
}),
password: value =>
validator.isStrongPassword(value)
};
function validate(data, schema) {
const errors = {};
for (const field in schema) {
const isValid =
schema[field](data[field]);
if (!isValid) {
errors[field] = 'Invalid value';
}
}
return errors;
}
function validateAge(age) {
const num = Number(age);
if (isNaN(num)) {
return false;
}
if (num < 18) {
return false;
}
return true;
}
function validateAge(age) {
return validator.isInt(age, {
min: 18
});
}
function isValidJson(value) {
try {
JSON.parse(value);
return true;
} catch {
return false;
}
}
function isValidJson(value) {
return validator.isJSON(value);
}
if (!validator.isEmail(email)) {
console.log('Email error');
}
if (!validator.isURL(site)) {
console.log('URL error');
}
function validateField(value, validatorFn, message) {
if (!validatorFn(value)) {
throw new Error(message);
}
}
validateField(
email,
validator.isEmail,
'Invalid email'
);
validateField(
site,
validator.isURL,
'Invalid URL'
);
function validate(data) {
if (data.email.indexOf('@') === -1) {
return false;
}
if (data.name.length < 2) {
return false;
}
return true;
}
function validate(data) {
if (!validator.isEmail(data.email)) {
return false;
}
if (!validator.isLength(data.name, {
min: 2
})) {
return false;
}
return true;
}
function validateEmail(email) {
return validator.isEmail(email);
}
function validateName(name) {
return validator.isLength(name, {
min: 2
});
}
src/
├── validators/
├── schemas/
├── middleware/
└── utils/
Плохо:
const executeValidationStrategyFactory =
createValidationExecutorFactory();
Хорошо:
validateEmail(email);
Плохо:
validateEverything(data);
Лучше:
validateUser(user);
validateProduct(product);
validateOrder(order);
Плохо:
function register(user) {
if (!validator.isEmail(user.email)) {
return;
}
saveUser(user);
}
Лучше:
validateUser(user);
saveUser(user);
Нежелательно переписывать всю систему валидации сразу.
Лучше:
Перед рефакторингом:
describe('validateEmail', () => {
test('валидный email', () => {
expect(
validateEmail('test@mail.com')
).toBe(true);
});
});
Рефакторинг не должен менять бизнес-логику.
Изменяется:
Не изменяется:
Было:
if (!value || value.length < 5)
Стало:
validator.isLength(value, { min: 5 })
Общая логика выносится в отдельные функции.
Было:
/^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i
Стало:
validator.isEmail(email)
Небольшие функции проще покрывать тестами.
test('validatePassword', () => {
expect(
validatePassword('Qwerty123')
).toBe(true);
});
function validateUser(data) {
const errors = {};
if (
!data.email ||
!/^.+@.+\..+$/.test(data.email)
) {
errors.email = 'Invalid email';
}
if (
!data.password ||
data.password.length < 8
) {
errors.password = 'Invalid password';
}
if (
!data.username ||
data.username.length < 3
) {
errors.username = 'Invalid username';
}
return errors;
}
export function validateEmail(email) {
return validator.isEmail(email || '');
}
export function validatePassword(password) {
return validator.isStrongPassword(password || '', {
minLength: 8,
minUppercase: 0,
minSymbols: 0
});
}
export function validateUsername(username) {
return validator.isLength(username || '', {
min: 3
});
}
import { validateEmail } from './email.js';
import { validatePassword } from './password.js';
import { validateUsername } from './username.js';
export function validateUser(data) {
const errors = {};
if (!validateEmail(data.email)) {
errors.email = 'Invalid email';
}
if (!validatePassword(data.password)) {
errors.password = 'Invalid password';
}
if (!validateUsername(data.username)) {
errors.username = 'Invalid username';
}
return errors;
}