Пошук уроків, статей та іншого контенту
Читайте, перевіряйте та обробляйте query parameters для фільтрації, пошуку й пагінації.
Query parameters — це параметри після знака ? в URL:
/api/products?category=books&search=javascript&page=2&pageSize=10У цьому прикладі:
category=books — фільтрація за категорією;
search=javascript — пошук;
page=2 — номер сторінки;
pageSize=10 — кількість елементів на сторінці.
Кілька параметрів розділяються символом &.
Query parameters не є частиною шляху маршруту. Наприклад:
/api/productsі:
/api/products?category=booksзвертаються до одного Route Handler, але передають різні параметри.
Важливо: усі значення query parameters надходять як рядки. Навіть page=2 спочатку має значення "2", тому перед використанням його потрібно перетворити на число й перевірити.
У Next.js App Router API endpoint створюється у файлі route.ts або route.js.
Наприклад:
app/api/products/route.tsУ Route Handler URL можна розібрати за допомогою стандартного класу URL:
export async function GET(request: Request) {
const url = new URL(request.url);
const searchParams = url.searchParams;
const search = searchParams.get("search");
return Response.json({
search,
});
}Для запиту:
/api/products?search=javascriptзначення search буде рядком "javascript".
Метод get() повертає:
значення параметра, якщо він переданий;
null, якщо параметра немає.
Тому зазвичай потрібно передбачити значення за замовчуванням:
const search = searchParams.get("search")?.trim() ?? "";Тут:
trim() прибирає зайві пробіли;
?? "" встановлює порожній рядок, якщо параметр відсутній.
URL може містити параметр кілька разів:
/api/products?category=books&category=gamesМетод get() поверне лише перше значення. Щоб отримати всі значення, використовуйте getAll():
const categories = searchParams.getAll("category");Результат:
["books", "games"]Не варто безпосередньо використовувати результат get() у математичних операціях. Спочатку потрібно перевірити, що значення є додатним цілим числом.
Наприклад:
const rawPage = searchParams.get("page");
const page = rawPage === null ? 1 : Number(rawPage);
if (!Number.isInteger(page) || page < 1) {
return Response.json(
{ error: "Параметр page має бути додатним цілим числом" },
{ status: 400 },
);
}Значення за замовчуванням для пагінації:
page — 1;
pageSize — наприклад, 10.
Також варто встановлювати максимальний розмір сторінки. Інакше клієнт може передати дуже велике значення, наприклад pageSize=100000.
Створимо endpoint:
app/api/products/route.tsВін підтримуватиме такі параметри:
category — фільтр за категорією;
search — пошук у назві;
page — номер сторінки;
pageSize — кількість товарів на сторінці.
type Category = "books" | "games" | "electronics";
type Product = {
id: number;
name: string;
category: Category;
};
const products: Product[] = [
{ id: 1, name: "JavaScript для початківців", category: "books" },
{ id: 2, name: "TypeScript Handbook", category: "books" },
{ id: 3, name: "The Legend of Zelda", category: "games" },
{ id: 4, name: "Ігрова клавіатура", category: "electronics" },
{ id: 5, name: "React Patterns", category: "books" },
{ id: 6, name: "Minecraft", category: "games" },
{ id: 7, name: "Бездротова миша", category: "electronics" },
{ id: 8, name: "Next.js у практиці", category: "books" },
];
const allowedCategories = new Set<Category>([
"books",
"games",
"electronics",
]);
function parsePositiveInteger(
value: string | null,
defaultValue: number,
): number | null {
if (value === null || value.trim() === "") {
return defaultValue;
}
const number = Number(value);
if (!Number.isInteger(number) || number < 1) {
return null;
}
return number;
}
export async function GET(request: Request) {
const url = new URL(request.url);
const searchParams = url.searchParams;
const rawCategory = searchParams.get("category");
const search = searchParams.get("search")?.trim().toLowerCase() ?? "";
const page = parsePositiveInteger(searchParams.get("page"), 1);
const requestedPageSize = parsePositiveInteger(
searchParams.get("pageSize"),
10,
);
if (page === null) {
return Response.json(
{ error: "Параметр page має бути додатним цілим числом" },
{ status: 400 },
);
}
if (requestedPageSize === null) {
return Response.json(
{ error: "Параметр pageSize має бути додатним цілим числом" },
{ status: 400 },
);
}
const pageSize = Math.min(requestedPageSize, 50);
if (
rawCategory !== null &&
!allowedCategories.has(rawCategory as Category)
) {
return Response.json(
{
error: "Невідома категорія",
allowedCategories: [...allowedCategories],
},
{ status: 400 },
);
}
const category = rawCategory as Category | null;
const filteredProducts = products.filter((product) => {
const matchesCategory =
category === null || product.category === category;
const matchesSearch =
search === "" || product.name.toLowerCase().includes(search);
return matchesCategory && matchesSearch;
});
const total = filteredProducts.length;
const totalPages = Math.ceil(total / pageSize);
const startIndex = (page - 1) * pageSize;
const items = filteredProducts.slice(startIndex, startIndex + pageSize);
return Response.json({
items,
pagination: {
page,
pageSize,
total,
totalPages,
},
});
}Після запуску застосунку можна виконати запит:
/api/products?category=books&search=javascript&page=1&pageSize=5Приклад відповіді:
{
"items": [
{
"id": 1,
"name": "JavaScript для початківців",
"category": "books"
}
],
"pagination": {
"page": 1,
"pageSize": 5,
"total": 1,
"totalPages": 1
}
}Фільтрація виконується до пагінації:
Отримуємо всі дані.
Застосовуємо фільтр за категорією.
Застосовуємо пошук.
Обчислюємо загальну кількість результатів.
Вибираємо елементи поточної сторінки.
Цей порядок важливий. Якщо застосувати пагінацію до фільтрації, кількість елементів на сторінці та загальна кількість результатів будуть неправильними.
У прикладі фільтр категорії є необов’язковим:
const matchesCategory =
category === null || product.category === category;Якщо category не переданий, проходять товари всіх категорій.
Пошук також є необов’язковим:
const matchesSearch =
search === "" || product.name.toLowerCase().includes(search);Порівняння виконується без урахування регістру, оскільки і пошуковий запит, і назва товару перетворюються на нижній регістр.
Основні обчислення:
const startIndex = (page - 1) * pageSize;
const items = filteredProducts.slice(startIndex, startIndex + pageSize);Для page=1 і pageSize=10:
startIndex = (1 - 1) * 10 = 0Будуть повернуті елементи з індексами від 0 до 9.
Для page=2:
startIndex = (2 - 1) * 10 = 10Будуть повернуті наступні 10 елементів.
Клієнту корисно повертати метадані пагінації:
{
"pagination": {
"page": 2,
"pageSize": 10,
"total": 35,
"totalPages": 4
}
}Якщо запитана сторінка більша за totalPages, slice() поверне порожній масив. Це дозволяє клієнту коректно показати відсутність результатів.
Для некоректних query parameters зазвичай повертають статус 400 Bad Request.
Наприклад:
{
"error": "Параметр page має бути додатним цілим числом"
}Статус 400 означає, що сервер отримав неправильний запит. Це відрізняється від ситуації, коли параметри правильні, але результатів немає. У такому випадку зазвичай повертають успішну відповідь із порожнім items:
{
"items": [],
"pagination": {
"page": 1,
"pageSize": 10,
"total": 0,
"totalPages": 0
}
}Небезпечно покладатися на те, що клієнт завжди передасть правильне число:
const page = Number(searchParams.get("page"));Якщо параметр відсутній або має значення abc, результатом буде NaN. Перевіряйте значення через Number.isInteger() і межі допустимих значень.
Ці запити різні:
/api/products
/api/products?search=У першому випадку get("search") повертає null, у другому — порожній рядок "". У більшості випадків їх можна обробляти однаково, але це потрібно зробити явно.
Спочатку потрібно отримати відфільтрований набір, і лише потім обчислювати total, totalPages та items.
pageSizeПараметр pageSize контролює обсяг відповіді. Встановлюйте максимальне значення, наприклад:
const pageSize = Math.min(requestedPageSize, 50);Навіть якщо параметр має бути одним із кількох значень, його потрібно перевірити:
const allowedCategories = new Set(["books", "games", "electronics"]);
if (category !== null && !allowedCategories.has(category)) {
// повернення помилки 400
}Не використовуйте довільне значення параметра без перевірки, особливо якщо воно впливає на запит до бази даних.
Query parameters доступні після ? і розділяються символом &.
У Route Handler їх можна прочитати через new URL(request.url).searchParams.
get() повертає перше значення або null, а getAll() — усі значення параметра.
Усі параметри надходять як рядки, тому числа потрібно перетворювати та валідувати.
Для фільтрації й пошуку спочатку формується відфільтрований набір даних.
Пагінація застосовується після фільтрації.
Некоректні параметри мають повертати відповідь зі статусом 400.
Для pageSize варто встановлювати максимальне допустиме значення.
Відповідь API має містити як список результатів, так і метадані пагінації.