Библиотека Vest предназначена для декларативной валидации данных и особенно хорошо подходит для серверных приложений, где требуется сложная логика проверки форм, API-запросов и бизнес-правил. В связке с Express Vest позволяет:
Типичная архитектура:
HTTP Request
↓
Express Middleware
↓
Vest Suite
↓
Validation Result
↓
Controller / Error Response
npm install vest express
Для современных проектов также часто устанавливаются:
npm install express-async-handler
Если используется TypeScript:
npm install -D typescript @types/express
project/
├── app.js
├── routes/
│ └── users.js
├── validation/
│ └── userValidation.js
├── middleware/
│ └── validate.js
└── controllers/
└── userController.js
Файл validation/userValidation.js:
import { create, test, enforce } fr om 'vest';
export const userSuite = create((data = {}) => {
test('email', 'Email обязателен', () => {
enforce(data.email).isNotBlank();
});
test('email', 'Некорректный email', () => {
enforce(data.email).matches(/^\S+@\S+\.\S+$/);
});
test('password', 'Минимум 8 символов', () => {
enforce(data.password).longerThanOrEquals(8);
});
test('age', 'Возраст должен быть больше 18', () => {
enforce(data.age).greaterThan(18);
});
});
Внутри suite описываются независимые проверки.
Каждый вызов test():
Express middleware — наиболее удобный способ подключения Vest.
Файл middleware/validate.js:
export function validate(suite) {
return (req, res, next) => {
const result = suite(req.body);
if (result.hasErrors()) {
return res.status(400).json({
success: false,
errors: result.getErrors(),
});
}
next();
};
}
Файл routes/users.js:
import express fr om 'express';
import { validate } fr om '../middleware/validate.js';
import { userSuite } from '../validation/userValidation.js';
const router = express.Router();
router.post(
'/register',
validate(userSuite),
(req, res) => {
res.json({
success: true,
message: 'Пользователь зарегистрирован',
});
}
);
export default router;
Файл app.js:
import express from 'express';
import userRoutes from './routes/users.js';
const app = express();
app.use(express.json());
app.use('/users', userRoutes);
app.listen(3000, () => {
console.log('Server started');
});
Метод getErrors() возвращает объект:
{
"email": [
"Email обязателен",
"Некорректный email"
],
"password": [
"Минимум 8 символов"
]
}
Такой формат идеально подходит для REST API.
Часто используется собственный error formatter.
export function validate(suite) {
return (req, res, next) => {
const result = suite(req.body);
if (!result.hasErrors()) {
return next();
}
const errors = Object.entries(result.getErrors())
.map(([field, messages]) => ({
field,
messages,
}));
return res.status(422).json({
success: false,
errors,
});
};
}
Ответ:
{
"success": false,
"errors": [
{
"field": "email",
"messages": [
"Некорректный email"
]
}
]
}
Vest можно применять не только к req.body.
export const searchSuite = create((query = {}) => {
test('page', 'Page должен быть числом', () => {
enforce(Number(query.page)).isNumber();
});
test('lim it', 'Lim it должен быть меньше 100', () => {
enforce(Number(query.lim it)).lessThanOrEquals(100);
});
});
Middleware:
router.get(
'/search',
validate(searchSuite),
controller
);
Передача query:
validate((data) => searchSuite(req.query))
Более удобный вариант — универсальный middleware.
export function validate(suite, selector = (req) => req.body) {
return (req, res, next) => {
const data = selector(req);
const result = suite(data);
if (result.hasErrors()) {
return res.status(400).json({
errors: result.getErrors(),
});
}
next();
};
}
Использование:
router.get(
'/search',
validate(searchSuite, req => req.query),
controller
);
export const idSuite = create((params = {}) => {
test('id', 'ID должен быть числом', () => {
enforce(Number(params.id)).isNumber();
});
});
Маршрут:
router.get(
'/:id',
validate(idSuite, req => req.params),
controller
);
Одно из ключевых преимуществ Vest — поддержка async-проверок.
Например, проверка существования email в базе.
import { create, test, enforce } from 'vest';
async function isEmailTaken(email) {
return email === 'admin@example.com';
}
export const registerSuite = create(async (data = {}) => {
test('email', 'Email обязателен', () => {
enforce(data.email).isNotBlank();
});
test(
'email',
'Email уже используется',
async () => {
const exists = await isEmailTaken(data.email);
enforce(exists).equals(false);
}
);
});
export function validateAsync(suite) {
return async (req, res, next) => {
const result = await suite(req.body);
if (result.hasErrors()) {
return res.status(400).json({
errors: result.getErrors(),
});
}
next();
};
}
Часто проверки выносятся в сервисы.
import { UserRepository } from '../repositories/UserRepository.js';
test(
'email',
'Email уже зарегистрирован',
async () => {
const user = await UserRepository.findByEmail(data.email);
enforce(user).isNull();
}
);
Преимущества:
Метод skipWhen позволяет отключать проверки.
import { skipWhen } from 'vest';
skipWhen(
!data.password,
() => {
test(
'password',
'Минимум 8 символов',
() => {
enforce(data.password)
.longerThanOrEquals(8);
}
);
}
);
Полезно для:
При обновлении сущности проверяются только переданные поля.
export const updateUserSuite = create((data = {}) => {
skipWhen(
data.email === undefined,
() => {
test(
'email',
'Некорректный email',
() => {
enforce(data.email)
.matches(/^\S+@\S+\.\S+$/);
}
);
}
);
skipWhen(
data.password === undefined,
() => {
test(
'password',
'Минимум 8 символов',
() => {
enforce(data.password)
.longerThanOrEquals(8);
}
);
}
);
});
export const addressSuite = create((data = {}) => {
test('address.city', 'Город обязателен', () => {
enforce(data.address?.city).isNotBlank();
});
test('address.street', 'Улица обязательна', () => {
enforce(data.address?.street).isNotBlank();
});
});
export const cartSuite = create((data = {}) => {
test('items', 'Корзина пуста', () => {
enforce(data.items).isArray();
enforce(data.items.length).greaterThan(0);
});
data.items?.forEach((item, index) => {
test(
`items[${index}].quantity`,
'Количество должно быть больше 0',
() => {
enforce(item.quantity).greaterThan(0);
}
);
});
});
Крупные приложения разделяют валидацию на модули.
export const emailSuite = create((data = {}) => {
test('email', 'Некорректный email', () => {
enforce(data.email)
.matches(/^\S+@\S+\.\S+$/);
});
});
export const passwordSuite = create((data = {}) => {
test('password', 'Слабый пароль', () => {
enforce(data.password)
.longerThanOrEquals(8);
});
});
Композиция:
export const registerSuite = create((data = {}) => {
emailSuite(data);
passwordSuite(data);
});
Vest поддерживает частичную валидацию.
const result = userSuite(data, fieldName);
Проверка только одного поля:
const result = userSuite(req.body, 'email');
Полезно для:
При использовании Multer:
export const uploadSuite = create((data = {}) => {
test('avatar', 'Файл обязателен', () => {
enforce(data.avatar).isNotNull();
});
test('avatar', 'Допустим только PNG', () => {
enforce(data.avatar.mimetype)
.equals('image/png');
});
test('avatar', 'Максимум 2MB', () => {
enforce(data.avatar.size)
.lessThanOrEquals(2 * 1024 * 1024);
});
});
export class ValidationError extends Error {
constructor(errors) {
super('Validation failed');
this.errors = errors;
this.status = 422;
}
}
Middleware:
export function validate(suite) {
return (req, res, next) => {
const result = suite(req.body);
if (result.hasErrors()) {
return next(
new ValidationError(
result.getErrors()
)
);
}
next();
};
}
Global handler:
app.use((err, req, res, next) => {
res.status(err.status || 500).json({
success: false,
message: err.message,
errors: err.errors || null,
});
});
Vest часто применяется после middleware авторизации.
router.post(
'/profile',
authMiddleware,
validate(profileSuite),
updateProfileController
);
Последовательность:
JWT Verify
↓
Validation
↓
Business Logic
import asyncHandler from 'express-async-handler';
router.post(
'/register',
asyncHandler(async (req, res) => {
const result = await registerSuite(req.body);
if (result.hasErrors()) {
return res.status(400).json({
errors: result.getErrors(),
});
}
res.json({
success: true,
});
})
);
export function validateEmail(field, value) {
test(field, 'Некорректный email', () => {
enforce(value)
.matches(/^\S+@\S+\.\S+$/);
});
}
Использование:
validateEmail('email', data.email);
export const roleSuite = create((data = {}) => {
test('role', 'Недопустимая роль', () => {
enforce(data.role).inside([
'admin',
'moderator',
'user',
]);
});
});
const messages = {
required: 'Поле обязательно',
invalidEmail: 'Некорректный email',
};
test(
'email',
messages.invalidEmail,
() => {
enforce(data.email)
.matches(/^\S+@\S+\.\S+$/);
}
);
Часто правила зависят от конфигурации приложения.
test(
'password',
'Пароль слишком короткий',
() => {
enforce(data.password)
.longerThanOrEquals(
Number(process.env.MIN_PASSWORD)
);
}
);
С использованием Jest:
import { registerSuite } from './registerSuite';
describe('Register validation', () => {
test('invalid email', () => {
const result = registerSuite({
email: 'wrong',
password: '12345678',
});
expect(
result.hasErrors('email')
).toBe(true);
});
});
Vest рекомендуется использовать только для:
Не рекомендуется:
Неправильно:
test('email', 'Ошибка', async () => {
await db.users.insert(data);
});
Для высоконагруженных API важны:
skipWhen(
result.hasErrors('email'),
() => {
test(
'email',
'Email занят',
async () => {
// expensive query
}
);
}
);
Большие validation-файлы ухудшают поддержку.
Оптимально:
validation/
├── auth/
├── users/
├── products/
└── orders/
const allowedRoles = ['admin', 'user'];
Вместо:
await loadRolesFromDatabase();
Request
↓
Helmet
↓
Rate Limiter
↓
JWT Middleware
↓
Vest Validation
↓
Controller
↓
Service
↓
Repository
↓
Database
import express from 'express';
import { create, test, enforce } from 'vest';
const app = express();
app.use(express.json());
const registerSuite = create(async (data = {}) => {
test(
'username',
'Имя обязательно',
() => {
enforce(data.username)
.isNotBlank();
}
);
test(
'email',
'Некорректный email',
() => {
enforce(data.email)
.matches(/^\S+@\S+\.\S+$/);
}
);
test(
'password',
'Минимум 8 символов',
() => {
enforce(data.password)
.longerThanOrEquals(8);
}
);
});
async function validate(req, res, next) {
const result = await registerSuite(req.body);
if (result.hasErrors()) {
return res.status(422).json({
success: false,
errors: result.getErrors(),
});
}
next();
}
app.post(
'/register',
validate,
async (req, res) => {
res.json({
success: true,
user: {
email: req.body.email,
},
});
}
);
app.listen(3000);