Работа с query параметрами

Query-параметры — часть URL, расположенная после символа ?. Они используются для передачи данных между клиентом и сервером:

https://example.com/users?page=2&limit=10

В данном URL:

  • page=2
  • limit=10

— это query-параметры.

В JavaScript query-параметры часто используются:

  • в Express.js;
  • в REST API;
  • при фильтрации и пагинации;
  • в формах поиска;
  • в SSR и SPA-приложениях;
  • при работе с URL браузера.

Поскольку query-параметры поступают извне, их необходимо валидировать. Библиотека Validator.js предоставляет для этого большой набор функций.


Получение query-параметров

Express.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 работает именно со строками, поэтому это поведение удобно.


Подключение Validator.js

Установка

npm install validator

Импорт

CommonJS

const validator = require('validator');

ES Modules

import validator fr om 'validator';

Проверка обязательных query-параметров

Проверка существования параметра

app.get('/search', (req, res) => {
    const { q } = req.query;

    if (!q) {
        return res.status(400).json({
            error: 'Параметр q обязателен'
        });
    }

    res.send('OK');
});

Однако такой подход недостаточен.

Проблема:

?q=

Параметр существует, но пустой.


Проверка пустых строк

validator.isEmpty()

validator.isEmpty(str)

Функция проверяет, является ли строка пустой.

Пример:

validator.isEmpty('');
// true

Проверка query-параметра

app.get('/search', (req, res) => {
    const q = req.query.q || '';

    if (validator.isEmpty(q)) {
        return res.status(400).json({
            error: 'Строка поиска пуста'
        });
    }

    res.send('OK');
});

Игнорирование пробелов

trim() + isEmpty()

Query-параметры могут содержать только пробелы:

?q=     

Проверка:

const q = (req.query.q || '').trim();

if (validator.isEmpty(q)) {
    return res.status(400).json({
        error: 'Некорректный запрос'
    });
}

Проверка числовых query-параметров

validator.isNumeric()

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');
});

Проверка целых чисел

validator.isInt()

isNumeric() пропускает:

12.5

Для целых чисел лучше использовать isInt().

validator.isInt('10');
// true

validator.isInt('10.5');
// false

Проверка page

const page = req.query.page || '1';

if (!validator.isInt(page)) {
    return res.status(400).json({
        error: 'Номер страницы должен быть целым числом'
    });
}

Ограничение диапазона

min и max

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');
});

Проверка float-значений

validator.isFloat()

validator.isFloat('10.5');
// true

Пример

const price = req.query.price;

if (!validator.isFloat(price)) {
    return res.status(400).json({
        error: 'price должен быть числом'
    });
}

Проверка диапазона float

validator.isFloat(price, {
    min: 0,
    max: 10000
});

Проверка boolean query-параметров

validator.isBoolean()

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'
    });
}

Преобразование boolean

После проверки параметр остаётся строкой.

const active = req.query.active === 'true';

Проверка email в query

validator.isEmail()

app.get('/invite', (req, res) => {
    const email = req.query.email || '';

    if (!validator.isEmail(email)) {
        return res.status(400).json({
            error: 'Некорректный email'
        });
    }

    res.send('OK');
});

Проверка URL

validator.isURL()

const website = req.query.website;

if (!validator.isURL(website)) {
    return res.status(400).json({
        error: 'Некорректный URL'
    });
}

Проверка UUID

validator.isUUID()

const id = req.query.id;

if (!validator.isUUID(id)) {
    return res.status(400).json({
        error: 'Некорректный UUID'
    });
}

Проверка даты

validator.isDate()

const date = req.query.date;

if (!validator.isDate(date)) {
    return res.status(400).json({
        error: 'Некорректная дата'
    });
}

Проверка JSON

validator.isJSON()

Иногда query-параметры содержат JSON:

/filter={"active":true}

Проверка:

const filter = req.query.filter;

if (!validator.isJSON(filter)) {
    return res.status(400).json({
        error: 'Некорректный JSON'
    });
}

Проверка длины строки

validator.isLength()

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 символов'
    });
}

Проверка whitelist значений

Ручная проверка массива

const sort = req.query.sort;

const allowed = ['asc', 'desc'];

if (!allowed.includes(sort)) {
    return res.status(400).json({
        error: 'Некорректная сортировка'
    });
}

validator.isIn()

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'
    });
}

Проверка slug

validator.isSlug()

validator.isSlug('my-post');
// true

Пример

const slug = req.query.slug;

if (!validator.isSlug(slug)) {
    return res.status(400).json({
        error: 'Некорректный slug'
    });
}

Проверка IP-адресов

validator.isIP()

validator.isIP('192.168.1.1');
// true

Query-параметр

const ip = req.query.ip;

if (!validator.isIP(ip)) {
    return res.status(400).json({
        error: 'Некорректный IP'
    });
}

Проверка массива query-параметров

Повторяющиеся параметры

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: 'Некорректная категория'
        });
    }
}

Нормализация query-параметров

Validator.js содержит функции нормализации данных.


validator.trim()

Удаление пробелов:

const q = validator.trim(req.query.q || '');

validator.escape()

Экранирование HTML-символов:

const q = validator.escape(req.query.q || '');

Пример:

<script>alert(1)</script>

Результат:

&lt;script&gt;alert(1)&lt;/script&gt;

validator.toInt()

const page = validator.toInt(req.query.page);

validator.toFloat()

const price = validator.toFloat(req.query.price);

validator.toBoolean()

const active = validator.toBoolean(req.query.active);

Полный пример middleware

Валидация query-параметров пользователей

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

Корректнее использовать ??.


Nullish coalescing

const page = req.query.page ?? '1';

Защита от слишком длинных query-параметров

Проблемный запрос:

?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: 'Некорректный запрос'
    });
}

Санитизация query-параметров

Нормализация email

const email = validator.normalizeEmail(
    req.query.email || ''
);

Приведение URL к безопасному виду

const url = validator.trim(req.query.url || '');

Частые ошибки

Отсутствие trim()

Ошибка:

validator.isEmpty('   ');
// false

Правильно:

validator.isEmpty('   '.trim());
// true

Проверка после преобразования

Ошибка:

validator.isInt(Number(page));

isInt() ожидает строку.

Правильно:

validator.isInt(page);

Игнорирование типов query-параметров

Ошибка:

if (req.query.page > 10)

Поскольку page — строка, возможны неожиданные результаты.

Корректнее:

const page = validator.toInt(req.query.page);

if (page > 10) {
    // ...
}

Практический пример API-фильтрации

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);
});