Пошук уроків, статей та іншого контенту
Додасте посторінкове отримання великих наборів даних із параметрами сторінки, розміру та метаданими результатів.
Пагінація — це поділ великого набору даних на невеликі частини, які повертаються окремими запитами.
Замість того щоб повертати всі записи одразу, клієнт вказує:
page — номер сторінки;
size — кількість записів на сторінці.
Наприклад:
GET /products?page=2&size=10Такий запит означає: повернути другу сторінку, що містить до 10 товарів.
Пагінація допомагає:
зменшити розмір HTTP-відповіді;
зменшити навантаження на сервер;
швидше відображати дані на клієнті;
не завантажувати непотрібні записи.
Для пагінації через зміщення використовується формула:
offset = (page - 1) * sizeде:
page — номер сторінки, починаючи з 1;
size — кількість записів на сторінці;
offset — кількість записів, які потрібно пропустити.
Приклад для page=3&size=10:
offset = (3 - 1) * 10 = 20Отже, потрібно пропустити перші 20 записів і повернути наступні 10.
У JavaScript для цього зручно використовувати slice:
const result = items.slice(offset, offset + size);Метод slice не змінює початковий масив.
Параметри пагінації надходять у рядку запиту URL:
/products?page=2&size=10У Node.js їх можна отримати через клас URL:
const requestUrl = new URL(request.url, `http://${request.headers.host}`);
const page = requestUrl.searchParams.get("page");
const size = requestUrl.searchParams.get("size");Значення параметрів із URL завжди є рядками або null, тому перед використанням їх потрібно перетворити на числа та перевірити.
Якщо параметри не передані, сервер може використати стандартні значення:
const page = Number(requestUrl.searchParams.get("page") ?? 1);
const size = Number(requestUrl.searchParams.get("size") ?? 10);У цьому прикладі:
сторінка за замовчуванням — 1;
розмір сторінки за замовчуванням — 10.
Клієнту не варто дозволяти вказувати необмежений size. Наприклад, запит із size=1000000 може створити зайве навантаження.
Зазвичай встановлюють максимальний розмір:
const MAX_PAGE_SIZE = 50;Якщо клієнт передав більший розмір, сервер може повернути помилку 400 Bad Request або обмежити значення до максимально дозволеного.
Самого масиву записів недостатньо. Клієнту потрібно знати:
загальну кількість записів;
поточну сторінку;
розмір сторінки;
загальну кількість сторінок;
чи існує наступна сторінка;
чи існує попередня сторінка.
Приклад структури відповіді:
{
"data": [
{
"id": 11,
"name": "Product 11"
}
],
"pagination": {
"page": 2,
"size": 10,
"totalItems": 57,
"totalPages": 6,
"hasNextPage": true,
"hasPreviousPage": true
}
}Загальну кількість сторінок можна обчислити так:
const totalPages = Math.ceil(totalItems / size);Наприклад, для 57 записів і розміру сторінки 10:
Math.ceil(57 / 10) = 6Остання сторінка міститиме лише 7 записів.
Нижче наведено самодостатній приклад на вбудованому модулі node:http. Він створює endpoint GET /products, підтримує параметри page і size, перевіряє їх та повертає дані разом із метаданими.
Створіть файл server.js:
const http = require("node:http");
const PORT = 3000;
const DEFAULT_PAGE = 1;
const DEFAULT_PAGE_SIZE = 10;
const MAX_PAGE_SIZE = 50;
// Демонстраційний набір даних.
// У реальному застосунку ці записи зазвичай отримують із бази даних.
const products = Array.from({ length: 57 }, (_, index) => ({
id: index + 1,
name: `Product ${index + 1}`,
price: (index + 1) * 10
}));
function sendJson(response, statusCode, data) {
response.writeHead(statusCode, {
"Content-Type": "application/json; charset=utf-8"
});
response.end(JSON.stringify(data));
}
function getPositiveInteger(value, defaultValue) {
if (value === null || value === "") {
return defaultValue;
}
const number = Number(value);
if (!Number.isInteger(number) || number < 1) {
return null;
}
return number;
}
const server = http.createServer((request, response) => {
const requestUrl = new URL(
request.url,
`http://${request.headers.host || "localhost"}`
);
if (request.method !== "GET" || requestUrl.pathname !== "/products") {
sendJson(response, 404, {
error: "Маршрут не знайдено"
});
return;
}
const page = getPositiveInteger(
requestUrl.searchParams.get("page"),
DEFAULT_PAGE
);
const size = getPositiveInteger(
requestUrl.searchParams.get("size"),
DEFAULT_PAGE_SIZE
);
if (page === null) {
sendJson(response, 400, {
error: "Параметр page має бути додатним цілим числом"
});
return;
}
if (size === null || size > MAX_PAGE_SIZE) {
sendJson(response, 400, {
error: `Параметр size має бути цілим числом від 1 до ${MAX_PAGE_SIZE}`
});
return;
}
const totalItems = products.length;
const totalPages = Math.ceil(totalItems / size);
const offset = (page - 1) * size;
const data = products.slice(offset, offset + size);
sendJson(response, 200, {
data,
pagination: {
page,
size,
totalItems,
totalPages,
hasNextPage: page < totalPages,
hasPreviousPage: page > 1
}
});
});
server.listen(PORT, () => {
console.log(`Сервер запущено на http://localhost:${PORT}`);
});Запустіть сервер:
node server.jsЗапит без параметрів використає значення за замовчуванням:
curl "http://localhost:3000/products"Запит другої сторінки з п'ятьма записами:
curl "http://localhost:3000/products?page=2&size=5"Приблизна відповідь:
{
"data": [
{
"id": 6,
"name": "Product 6",
"price": 60
},
{
"id": 7,
"name": "Product 7",
"price": 70
},
{
"id": 8,
"name": "Product 8",
"price": 80
},
{
"id": 9,
"name": "Product 9",
"price": 90
},
{
"id": 10,
"name": "Product 10",
"price": 100
}
],
"pagination": {
"page": 2,
"size": 5,
"totalItems": 57,
"totalPages": 12,
"hasNextPage": true,
"hasPreviousPage": true
}
}Наприклад, для 57 записів і size=10 існує 6 сторінок. Якщо клієнт запитає:
GET /products?page=20&size=10вираз:
products.slice(190, 200);поверне порожній масив.
Це допустима поведінка, якщо сторінка є коректним додатним числом. Метадані покажуть, що:
{
"page": 20,
"totalPages": 6,
"hasNextPage": false,
"hasPreviousPage": true
}Інший підхід — повертати 404, якщо сторінки не існує. Головне — обрати єдину поведінку та дотримуватися її в API.
У прикладі весь набір даних зберігається в масиві, а slice вибирає потрібну частину. Для великих наборів даних не слід щоразу завантажувати всю таблицю в пам'ять Node.js.
База даних має отримувати лише потрібний діапазон записів. Загальна логіка залишається такою самою:
offset = (page - 1) * sizeДля SQL-запиту це зазвичай відповідає конструкції:
SELECT id, name, price
FROM products
ORDER BY id
LIMIT 10 OFFSET 20;Окремо потрібно отримати загальну кількість записів:
SELECT COUNT(*)
FROM products;У виробничому застосунку значення totalItems і самі записи можуть отримуватися окремими запитами до бази даних. Порядок ORDER BY важливий: без стабільного сортування склад сторінок може змінюватися між запитами.
Параметри пагінації потрібно перевіряти до виконання запиту.
Коректні значення:
?page=1&size=10
?page=3&size=25Некоректні значення:
?page=0&size=10
?page=-1&size=10
?page=abc&size=10
?page=1&size=0
?page=1&size=1000Для некоректних параметрів сервер має повернути статус 400 Bad Request і зрозуміле повідомлення про помилку.
Якщо API використовує сторінки, що починаються з 1, формула має вигляд:
const offset = (page - 1) * size;Помилка в цій формулі призводить до пропуску записів або дублювання даних.
sizeНе можна без перевірки використовувати значення, передане клієнтом:
const size = Number(requestUrl.searchParams.get("size"));Клієнт може передати дуже велике число або нечислове значення. Потрібні значення за замовчуванням, перевірка та максимальний ліміт.
Параметри сторінки та розміру повинні бути цілими:
?page=1.5&size=10Таке значення потрібно відхилити, а не автоматично округляти.
Якщо набір даних змінюється між запитами, записи можуть переміститися на інші сторінки. Для передбачуваного результату дані потрібно повертати в стабільному порядку, наприклад за унікальним id.
Відповідь такого вигляду:
[
{
"id": 1,
"name": "Product 1"
}
]не повідомляє клієнту, скільки всього існує записів і чи є наступна сторінка. Для зручної роботи клієнта повертайте масив даних разом із метаданими пагінації.
Пагінація розділяє великий набір даних на сторінки.
Основні параметри — page і size.
Зміщення обчислюється за формулою (page - 1) * size.
Для кількості сторінок використовується Math.ceil(totalItems / size).
Відповідь має містити як дані, так і метадані.
Параметри потрібно перевіряти та обмежувати максимальний розмір сторінки.
Дані слід повертати у стабільному порядку.
Під час роботи з базою даних потрібно отримувати лише потрібну сторінку, а не весь набір записів.