Query-параметры — часть URL, расположенная после символа
?. Они используются для передачи данных между клиентом и
сервером:
https://example.com/users?page=2&limit=10
В данном URL:
page=2limit=10— это query-параметры.
В JavaScript query-параметры часто используются:
Поскольку query-параметры поступают извне, их необходимо валидировать. Библиотека Validator.js предоставляет для этого большой набор функций.
app.get('/users', (req, res) => {
console.log(req.query);
});
URL:
/users?page=3&sort=desc
Результат:
{
page: '3',
sort: 'desc'
}
Важно понимать: значения query-параметров всегда приходят строками.
Даже если параметр выглядит как число:
?page=10
в Express это:
'10'
Validator.js работает именно со строками, поэтому это поведение удобно.
npm install validator
const validator = require('validator');
import validator fr om 'validator';
app.get('/search', (req, res) => {
const { q } = req.query;
if (!q) {
return res.status(400).json({
error: 'Параметр q обязателен'
});
}
res.send('OK');
});
Однако такой подход недостаточен.
Проблема:
?q=
Параметр существует, но пустой.
validator.isEmpty(str)
Функция проверяет, является ли строка пустой.
Пример:
validator.isEmpty('');
// true
app.get('/search', (req, res) => {
const q = req.query.q || '';
if (validator.isEmpty(q)) {
return res.status(400).json({
error: 'Строка поиска пуста'
});
}
res.send('OK');
});
Query-параметры могут содержать только пробелы:
?q=
Проверка:
const q = (req.query.q || '').trim();
if (validator.isEmpty(q)) {
return res.status(400).json({
error: 'Некорректный запрос'
});
}
validator.isNumeric('123');
// true
app.get('/users', (req, res) => {
const page = req.query.page || '1';
if (!validator.isNumeric(page)) {
return res.status(400).json({
error: 'page должен быть числом'
});
}
res.send('OK');
});
isNumeric() пропускает:
12.5
Для целых чисел лучше использовать isInt().
validator.isInt('10');
// true
validator.isInt('10.5');
// false
const page = req.query.page || '1';
if (!validator.isInt(page)) {
return res.status(400).json({
error: 'Номер страницы должен быть целым числом'
});
}
validator.isInt(page, {
min: 1,
max: 100
});
app.get('/products', (req, res) => {
const page = req.query.page || '1';
if (!validator.isInt(page, {
min: 1,
max: 50
})) {
return res.status(400).json({
error: 'page должен быть от 1 до 50'
});
}
res.send('OK');
});
validator.isFloat('10.5');
// true
const price = req.query.price;
if (!validator.isFloat(price)) {
return res.status(400).json({
error: 'price должен быть числом'
});
}
validator.isFloat(price, {
min: 0,
max: 10000
});
validator.isBoolean('true');
// true
validator.isBoolean('false');
// true
const active = req.query.active;
if (!validator.isBoolean(active)) {
return res.status(400).json({
error: 'active должен быть boolean'
});
}
После проверки параметр остаётся строкой.
const active = req.query.active === 'true';
app.get('/invite', (req, res) => {
const email = req.query.email || '';
if (!validator.isEmail(email)) {
return res.status(400).json({
error: 'Некорректный email'
});
}
res.send('OK');
});
const website = req.query.website;
if (!validator.isURL(website)) {
return res.status(400).json({
error: 'Некорректный URL'
});
}
const id = req.query.id;
if (!validator.isUUID(id)) {
return res.status(400).json({
error: 'Некорректный UUID'
});
}
const date = req.query.date;
if (!validator.isDate(date)) {
return res.status(400).json({
error: 'Некорректная дата'
});
}
Иногда query-параметры содержат JSON:
/filter={"active":true}
Проверка:
const filter = req.query.filter;
if (!validator.isJSON(filter)) {
return res.status(400).json({
error: 'Некорректный JSON'
});
}
validator.isLength(str, {
min: 3,
max: 20
});
const q = req.query.q || '';
if (!validator.isLength(q, {
min: 3,
max: 50
})) {
return res.status(400).json({
error: 'Длина запроса должна быть от 3 до 50 символов'
});
}
const sort = req.query.sort;
const allowed = ['asc', 'desc'];
if (!allowed.includes(sort)) {
return res.status(400).json({
error: 'Некорректная сортировка'
});
}
validator.isIn(sort, ['asc', 'desc']);
const sort = req.query.sort || 'asc';
if (!validator.isIn(sort, ['asc', 'desc'])) {
return res.status(400).json({
error: 'sort должен быть asc или desc'
});
}
validator.isSlug('my-post');
// true
const slug = req.query.slug;
if (!validator.isSlug(slug)) {
return res.status(400).json({
error: 'Некорректный slug'
});
}
validator.isIP('192.168.1.1');
// true
const ip = req.query.ip;
if (!validator.isIP(ip)) {
return res.status(400).json({
error: 'Некорректный IP'
});
}
URL:
/products?category=books&category=movies
Express:
{
category: ['books', 'movies']
}
const categories = req.query.category;
if (!Array.isArray(categories)) {
return res.status(400).json({
error: 'category должен быть массивом'
});
}
for (const item of categories) {
if (!validator.isSlug(item)) {
return res.status(400).json({
error: 'Некорректная категория'
});
}
}
Validator.js содержит функции нормализации данных.
Удаление пробелов:
const q = validator.trim(req.query.q || '');
Экранирование HTML-символов:
const q = validator.escape(req.query.q || '');
Пример:
<script>alert(1)</script>
Результат:
<script>alert(1)</script>
const page = validator.toInt(req.query.page);
const price = validator.toFloat(req.query.price);
const active = validator.toBoolean(req.query.active);
const validator = require('validator');
function validateUsersQuery(req, res, next) {
const page = req.query.page || '1';
const lim it = req.query.limit || '10';
const sort = req.query.sort || 'asc';
if (!validator.isInt(page, {
min: 1
})) {
return res.status(400).json({
error: 'Некорректный page'
});
}
if (!validator.isInt(limit, {
min: 1,
max: 100
})) {
return res.status(400).json({
error: 'Некорректный limit'
});
}
if (!validator.isIn(sort, ['asc', 'desc'])) {
return res.status(400).json({
error: 'Некорректный sort'
});
}
req.query.page = validator.toInt(page);
req.query.limit = validator.toInt(limit);
next();
}
Использование:
app.get('/users', validateUsersQuery, (req, res) => {
console.log(req.query);
res.send('OK');
});
const querySchema = {
page: value =>
validator.isInt(value, {
min: 1
}),
limit: value =>
validator.isInt(value, {
min: 1,
max: 100
}),
sort: value =>
validator.isIn(value, ['asc', 'desc'])
};
function validateQuery(query, schema) {
const errors = {};
for (const key in schema) {
const value = query[key];
const isValid = schema[key](value);
if (!isValid) {
errors[key] = 'Некорректное значение';
}
}
return errors;
}
app.get('/users', (req, res) => {
const errors = validateQuery(req.query, querySchema);
if (Object.keys(errors).length > 0) {
return res.status(400).json({
errors
});
}
res.send('OK');
});
Не все query-параметры обязательны.
const email = req.query.email;
if (email && !validator.isEmail(email)) {
return res.status(400).json({
error: 'Некорректный email'
});
}
const limit = req.query.limit || '10';
Конструкция:
const page = req.query.page || '1';
может работать некорректно:
?page=0
Значение '0' является truthy, поэтому ошибки нет.
Но если используется число:
const page = Number(req.query.page) || 1;
то:
Number('0')
// 0
и результатом станет:
1
Корректнее использовать ??.
const page = req.query.page ?? '1';
Проблемный запрос:
?q=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
Ограничение длины:
if (!validator.isLength(q, {
max: 100
})) {
return res.status(400).json({
error: 'Слишком длинный параметр'
});
}
const q = validator.trim(req.query.q || '');
if (
validator.isEmpty(q) ||
!validator.isLength(q, {
min: 3,
max: 50
})
) {
return res.status(400).json({
error: 'Некорректный запрос'
});
}
const email = validator.normalizeEmail(
req.query.email || ''
);
const url = validator.trim(req.query.url || '');
Ошибка:
validator.isEmpty(' ');
// false
Правильно:
validator.isEmpty(' '.trim());
// true
Ошибка:
validator.isInt(Number(page));
isInt() ожидает строку.
Правильно:
validator.isInt(page);
Ошибка:
if (req.query.page > 10)
Поскольку page — строка, возможны неожиданные
результаты.
Корректнее:
const page = validator.toInt(req.query.page);
if (page > 10) {
// ...
}
app.get('/products', (req, res) => {
const page = req.query.page ?? '1';
const limit = req.query.limit ?? '10';
const category = req.query.category ?? '';
const minPrice = req.query.minPrice;
const maxPrice = req.query.maxPrice;
if (!validator.isInt(page, { min: 1 })) {
return res.status(400).json({
error: 'Некорректный page'
});
}
if (!validator.isInt(limit, {
min: 1,
max: 100
})) {
return res.status(400).json({
error: 'Некорректный limit'
});
}
if (
category &&
!validator.isSlug(category)
) {
return res.status(400).json({
error: 'Некорректная category'
});
}
if (
minPrice &&
!validator.isFloat(minPrice, {
min: 0
})
) {
return res.status(400).json({
error: 'Некорректный minPrice'
});
}
if (
maxPrice &&
!validator.isFloat(maxPrice, {
min: 0
})
) {
return res.status(400).json({
error: 'Некорректный maxPrice'
});
}
const filters = {
page: validator.toInt(page),
limit: validator.toInt(limit),
category,
minPrice: minPrice
? validator.toFloat(minPrice)
: null,
maxPrice: maxPrice
? validator.toFloat(maxPrice)
: null
};
res.json(filters);
});