Фильтрация, сортировка, поиск
Урок о том, как дать клиенту фильтровать, сортировать и искать через query-параметры, не превращая API в неуправляемого монстра.
Query-параметры — пары ключ=значение после
?в URL, которыми клиент уточняет, какие именно записи коллекции он хочет получить, в каком порядке и с какими полями.
Эндпоинт GET /v1/orders возвращает все заказы — но клиенту почти никогда не нужны «все». Ему нужны заказы со статусом active, дороже 1000 рублей, отсортированные по дате убывания, и только поля id и total. Хорошо спроектированные query-параметры позволяют выразить это в URL, не плодя десятки специализированных эндпоинтов вроде /orders/active-expensive-recent. Плохо спроектированные — превращаются в мини-язык запросов, который невозможно валидировать и который открывает дыры в производительности и безопасности.
Фильтрация по точному совпадению
Базовый случай — фильтр по равенству. Имя поля становится ключом параметра:
GET /v1/orders?status=active
GET /v1/orders?status=active¤cy=RUB
Несколько параметров комбинируются через логическое И: первый запрос вернёт активные заказы, второй — активные и в рублях. Под капотом это превращается в WHERE status = 'active' AND currency = 'RUB'. Простой, предсказуемый, легко документируемый паттерн.
Диапазоны
Равенства мало — часто нужно «дороже», «не позже». Распространённое соглашение — суффиксы операторов:
GET /v1/orders?price_gte=1000 -- цена >= 1000
GET /v1/orders?price_lte=5000 -- цена <= 5000
GET /v1/orders?price_gte=1000&price_lte=5000 -- диапазон
GET /v1/orders?created_after=2026-01-01 -- по дате
Суффиксы _gte/_lte/_gt/_lt (greater/less than or equal) читаются однозначно и ложатся на SQL price >= 1000 AND price <= 5000. Альтернатива — отдельные имена вроде min_price/max_price: они нагляднее для людей, но хуже масштабируются на много полей. Выберите одно соглашение и применяйте единообразно.
Сортировка
Удобная и компактная конвенция — один параметр sort с префиксом направления: минус означает по убыванию, его отсутствие — по возрастанию.
GET /v1/orders?sort=created_at -- по дате, по возрастанию
GET /v1/orders?sort=-created_at -- по дате, по убыванию (новые сверху)
GET /v1/orders?sort=-priority,created_at -- сначала по priority убыв., затем по дате возр.
Перечисление через запятую задаёт многоуровневую сортировку. Это компактнее пары параметров sort_by=created_at&order=desc и хорошо расширяется. Критично: сортировать можно только по белому списку полей — об этом ниже, в валидации.
Выбор полей (sparse fieldsets)
Если клиенту нужны лишь пара полей из жирного объекта, заставлять его качать всё — расточительно. Параметр fields позволяет запросить подмножество:
GET /v1/orders?fields=id,total,status
{
"data": [
{ "id": 41, "total": 1500, "status": "active" },
{ "id": 42, "total": 3200, "status": "active" }
]
}
Это экономит трафик и ускоряет мобильных клиентов. Цена — усложнение сервера: нужно проверять, что запрошенные поля существуют и разрешены, и аккуратно строить ответ. Не перегружайте этим API на старте — добавляйте, когда трафик реально станет проблемой.
Полнотекстовый поиск
Фильтры — это точные условия; поиск — это «найди что-то похожее на текст». Их стоит разделять. Конвенция — параметр q (query):
GET /v1/products?q=беспроводные наушники
GET /v1/products?q=наушники&category=audio&sort=-rating
За q обычно стоит не LIKE '%...%' (он не масштабируется и не ранжирует), а полнотекстовый движок: встроенный full-text в PostgreSQL/MySQL или внешний Elasticsearch/OpenSearch. Поиск и фильтры отлично сочетаются: q сужает по релевантности, остальные параметры — по точным условиям.
Проектирование без взрыва сложности
Соблазн велик: разрешить фильтровать по любому полю любым оператором, вложенные условия, OR-группы. Так рождается самодельный язык запросов в URL — его невозможно ни задокументировать, ни обезопасить. Держите дисциплину:
- Белый список. Заранее решите, по каким полям можно фильтровать и сортировать. Остальное — игнорируйте или отвечайте
400. - Плоскость. Избегайте вложенных условий и произвольных OR. Если нужны сложные запросы — это сигнал к отдельному search-эндпоинту с телом, а не к нагромождению query-параметров.
- Стабильные имена. Имена параметров — часть контракта API; менять их так же больно, как ломать поля ответа.
Валидация и дефолты
Каждый входящий параметр — недоверенный ввод. Правила гигиены:
| Параметр | Правило |
status | только из набора (active/paid/cancelled); иное → 400 |
sort | поля только из белого списка; неизвестное поле → 400 |
price_gte | должно парситься в число; price_gte=abc → 400 |
limit | дефолт 20, максимум 100; отрицательное → 400 |
fields | только существующие и разрешённые поля |
Дефолты важны не меньше валидации: если клиент не указал sort, выберите осмысленный порядок по умолчанию (например, -created_at) — без него БД вернёт строки в недетерминированном порядке, и пагинация поедет. Дефолтный и максимальный limit защищают от случайной выгрузки всей таблицы.
Как работает под капотом
Сервер не подставляет query-значения в SQL напрямую — это прямой путь к SQL-инъекции. Слой обработки сначала валидирует каждый параметр против схемы (тип, диапазон, принадлежность белому списку), затем строит запрос через параметризованные плейсхолдеры: имя столбца берётся из доверенного белого списка, а значение уходит отдельным параметром драйвера БД. Поэтому белый список полей сортировки — не только про чистоту API, но и про безопасность: имя столбца нельзя параметризовать как значение, его можно только выбрать из заранее известного множества. Производительность тоже определяется здесь: фильтр по неиндексированному полю заставляет БД делать полный скан таблицы. Грамотный API разрешает фильтровать ровно по тем полям, под которые есть индексы, — иначе безобидный с виду ?some_field=x на большой таблице станет тормозом. Сортировка по неиндексированному полю аналогично требует дорогой сортировки всего результата в памяти.
Частые ошибки
- Фильтр по чему угодно. Разрешить произвольные поля — открыть полный скан по неиндексированным столбцам и риск инъекции.
- Нет дефолтной сортировки. Без
ORDER BYпорядок строк недетерминирован, и пагинация показывает дубли/пропуски. - Поиск и фильтр в одном параметре. Смешивать полнотекстовый
qс точными условиями в одном поле — путаница; разделяйте их. - Молчаливое игнорирование опечаток. Если
?statuss=activeтихо вернёт всё, клиент решит, что фильтр сработал. Либо валидируйте строго (400), либо чётко документируйте политику игнора. - Самодельный язык запросов. Вложенные OR/AND в URL невозможно безопасно разобрать — для сложного выноси́те поиск в отдельный POST-эндпоинт с телом запроса.
Итоги
- Фильтры — точные условия (
?status=active), диапазоны — суффиксы (?price_gte=1000), комбинируются через И. - Сортировка — компактный
?sort=-created_atс минусом для убывания и запятыми для нескольких ключей. fieldsэкономит трафик,q— отдельный полнотекстовый поиск; не смешивайте поиск с фильтрами.- Держите белый список полей и плоскую структуру — не растите язык запросов в URL.
- Валидируйте каждый параметр, задавайте дефолты (особенно сортировку и
limit) и фильтруйте только по индексированным полям.