Пагинация и фильтрация
Пагинация — разбиение большого набора данных на страницы; фильтрация — выборка только тех записей, которые удовлетворяют условию.
Зачем нужно
Возвращать все записи одним запросом нельзя: 100 000 продуктов убьют память сервера, перегрузят сеть и замедлят клиент. Пагинация и фильтрация — основной механизм работы с большими данными в REST API. Правильная реализация влияет на производительность БД, скорость отклика API и UX.
Где используется
- Любые списочные API-эндпоинты (пользователи, товары, заказы)
- Таблицы и списки в пользовательском интерфейсе
- Бесконечная прокрутка (infinite scroll) — cursor-based пагинация
- Поиск и фильтры в e-commerce, admin-панелях
Виды пагинации
1. Offset-based (страничная)
GET /api/products?page=3&limit=20
GET /api/products?offset=40&limit=20
// Сервер — Express + SQL (условно)
app.get('/api/products', async (req, res) => {
const page = Math.max(1, parseInt(req.query.page) || 1);
const limit = Math.min(100, parseInt(req.query.limit) || 20);
const offset = (page - 1) * limit;
const [items, total] = await Promise.all([
db.query('SELECT * FROM products LIMIT ? OFFSET ?', [limit, offset]),
db.query('SELECT COUNT(*) as cnt FROM products'),
]);
res.json({
data: items,
pagination: {
page,
limit,
total: total[0].cnt,
totalPages: Math.ceil(total[0].cnt / limit),
hasNext: page * limit < total[0].cnt,
hasPrev: page > 1,
},
});
});
2. Cursor-based (для infinite scroll)
GET /api/posts?limit=20
GET /api/posts?cursor=eyJpZCI6NDJ9&limit=20
// Cursor — обычно base64(JSON) от последнего элемента
app.get('/api/posts', async (req, res) => {
const limit = parseInt(req.query.limit) || 20;
const cursor = req.query.cursor
? JSON.parse(Buffer.from(req.query.cursor, 'base64').toString())
: null;
const where = cursor ? `WHERE id < ${cursor.id}` : '';
const posts = await db.query(
`SELECT * FROM posts ${where} ORDER BY id DESC LIMIT ?`,
[limit + 1]
);
const hasNext = posts.length > limit;
if (hasNext) posts.pop();
const nextCursor = hasNext
? Buffer.from(JSON.stringify({ id: posts.at(-1).id })).toString('base64')
: null;
res.json({ data: posts, nextCursor });
});
Фильтрация
# Простые фильтры через query-параметры
GET /api/products?category=electronics&in_stock=true&min_price=1000&max_price=50000
# Сортировка (- = убывание)
GET /api/products?sort=-price,name
# Поиск по тексту
GET /api/products?search=ноутбук
# Выбор полей
GET /api/products?fields=id,name,price,image
// Безопасная сборка фильтров
app.get('/api/products', async (req, res) => {
const { category, min_price, max_price, in_stock, search, sort, fields } = req.query;
const conditions = ;
const params = ;
if (category) { conditions.push('category = ?'); params.push(category); }
if (min_price) { conditions.push('price >= ?'); params.push(+min_price); }
if (max_price) { conditions.push('price <= ?'); params.push(+max_price); }
if (in_stock === 'true') { conditions.push('stock > 0'); }
if (search) { conditions.push('name LIKE ?'); params.push(`%${search}%`); }
const where = conditions.length ? `WHERE ${conditions.join(' AND ')}` : '';
// Whitelist сортировки — защита от SQL injection
const ALLOWED_SORTS = ['price', '-price', 'name', '-name', 'created_at'];
const orderField = ALLOWED_SORTS.includes(sort)
? sort.replace('-', '') + (sort.startsWith('-') ? ' DESC' : ' ASC')
: 'id ASC';
const products = await db.query(
`SELECT * FROM products ${where} ORDER BY ${orderField}`,
params
);
res.json({ data: products });
});
Частые ошибки
- Нет ограничения максимального
limit— клиент запрашиваетlimit=999999 - SQL injection через поле
sort—sort=price; DROP TABLE products - Offset-пагинация на больших таблицах —
OFFSET 100000медленно, лучше cursor - Возврат
totalбез пагинации — пересчёт COUNT на каждый запрос дорого