Пошук уроків, статей та іншого контенту
Побудуєте пошук ресурсів за текстовими полями, параметрами запиту та правилами обробки порожніх результатів.
Пошук у REST API зазвичай реалізують через параметри запиту URL. Наприклад:
GET /books?q=nodeТут:
/books — ресурс;
q — параметр пошуку;
node — пошуковий запит.
Параметри запиту не є частиною шляху ресурсу. Вони уточнюють, які саме ресурси потрібно повернути.
Наприклад:
GET /books?q=node&author=martinТакий запит може означати:
знайти книги, у назві або описі яких є node;
додатково залишити лише книги потрібного автора.
Перед реалізацією потрібно визначити правила пошуку. Наприклад, для ресурсу books можна використати такі параметри:
q — текст для пошуку в назві та описі;
author — пошук за автором;
genre — фільтрація за жанром.
Приклад запиту:
GET /books?q=javascript&genre=programmingУсі передані параметри застосовуються одночасно. Тобто книга має відповідати і текстовому пошуку, і жанру.
Пошуковий параметр q може перевіряти кілька полів:
q="node"Результат має містити книги, у яких node зустрічається:
у title;
або в description.
Пошук зазвичай роблять нечутливим до регістру. Тому запити node, Node і NODE мають давати однаковий результат.
Значення параметра потрібно очистити від зайвих пробілів:
" node " → "node"Це запобігає ситуаціям, коли однакові на вигляд запити поводяться по-різному.
Якщо після очищення параметр порожній, наприклад:
GET /books?q=можна трактувати його так само, як відсутній параметр q: повернути всі книги. Важливо обрати одне правило й послідовно його використовувати.
Нижче наведено повністю працездатний сервер без додаткових бібліотек. Він підтримує:
GET /books;
пошук через q у полях title і description;
фільтрацію через author і genre;
порожній результат із HTTP-статусом 200;
JSON-відповіді.
Створіть файл server.js:
const http = require('node:http');
const { URL } = require('node:url');
const books = [
{
id: 1,
title: 'Node.js Design Patterns',
author: 'Mario Casciaro',
genre: 'programming',
description: 'Практичний посібник із побудови застосунків на Node.js.'
},
{
id: 2,
title: 'JavaScript: The Good Parts',
author: 'Douglas Crockford',
genre: 'programming',
description: 'Короткий огляд основних можливостей JavaScript.'
},
{
id: 3,
title: 'Clean Code',
author: 'Robert C. Martin',
genre: 'software',
description: 'Принципи написання зрозумілого та підтримуваного коду.'
},
{
id: 4,
title: 'The Pragmatic Programmer',
author: 'David Thomas',
genre: 'software',
description: 'Практичні підходи до розробки програмного забезпечення.'
}
];
function sendJson(response, statusCode, body) {
response.writeHead(statusCode, {
'Content-Type': 'application/json; charset=utf-8'
});
response.end(JSON.stringify(body));
}
function normalize(value) {
return value.trim().toLocaleLowerCase();
}
function searchBooks(searchParams) {
const query = normalize(searchParams.get('q') || '');
const author = normalize(searchParams.get('author') || '');
const genre = normalize(searchParams.get('genre') || '');
return books.filter((book) => {
const matchesQuery =
query === '' ||
normalize(book.title).includes(query) ||
normalize(book.description).includes(query);
const matchesAuthor =
author === '' ||
normalize(book.author).includes(author);
const matchesGenre =
genre === '' ||
normalize(book.genre) === genre;
return matchesQuery && matchesAuthor && matchesGenre;
});
}
const server = http.createServer((request, response) => {
const requestUrl = new URL(
request.url,
`http://${request.headers.host || 'localhost'}`
);
if (request.method !== 'GET' || requestUrl.pathname !== '/books') {
sendJson(response, 404, {
error: 'Маршрут не знайдено'
});
return;
}
const result = searchBooks(requestUrl.searchParams);
sendJson(response, 200, {
data: result,
meta: {
count: result.length
}
});
});
server.listen(3000, () => {
console.log('Сервер запущено на http://localhost:3000');
});Запустіть сервер:
node server.jsПісля цього він буде доступний за адресою:
http://localhost:3000У Node.js об’єкт URL дозволяє розібрати адресу запиту:
const requestUrl = new URL(
request.url,
`http://${request.headers.host || 'localhost'}`
);Для адреси:
/books?q=node&genre=programmingможна отримати параметри через searchParams:
const query = requestUrl.searchParams.get('q');
const genre = requestUrl.searchParams.get('genre');Метод get() повертає:
рядок зі значенням параметра;
null, якщо параметр відсутній.
Тому в прикладі використано значення за замовчуванням:
const query = searchParams.get('q') || '';Після цього значення можна безпечно передати до trim().
Основна умова для q виглядає так:
const matchesQuery =
query === '' ||
normalize(book.title).includes(query) ||
normalize(book.description).includes(query);Вона читається так:
Якщо пошуковий запит порожній — книга підходить.
Інакше перевіряється назва.
Якщо назва не підходить — перевіряється опис.
Оператори || означають «або». Тому достатньо збігу хоча б в одному текстовому полі.
Для автора використовується інша умова:
const matchesAuthor =
author === '' ||
normalize(book.author).includes(author);Для жанру використано точне порівняння:
const matchesGenre =
genre === '' ||
normalize(book.genre) === genre;Це різні правила:
author може містити частковий збіг;
genre має повністю збігатися зі значенням жанру.
Усі умови об’єднано оператором &&:
return matchesQuery && matchesAuthor && matchesGenre;Оператор && означає «і». Книга потрапить до результату лише тоді, коли відповідає всім активним фільтрам.
Наприклад, запит:
GET /books?q=programming&author=martinвиконується так:
шукається programming у назві або описі;
результат додатково перевіряється за автором;
повертаються лише книги, які відповідають обом умовам.
Пошук за текстом у назві або описі:
curl "http://localhost:3000/books?q=node"Приклад відповіді:
{
"data": [
{
"id": 1,
"title": "Node.js Design Patterns",
"author": "Mario Casciaro",
"genre": "programming",
"description": "Практичний посібник із побудови застосунків на Node.js."
}
],
"meta": {
"count": 1
}
}Фільтрація за автором:
curl "http://localhost:3000/books?author=martin"Комбінація текстового пошуку та жанру:
curl "http://localhost:3000/books?q=code&genre=software"Параметри можна передавати в іншому порядку:
curl "http://localhost:3000/books?genre=software&q=code"Для сервера порядок параметрів не має значення.
Якщо жоден ресурс не відповідає пошуку, це не помилка сервера. Правильна відповідь — HTTP 200 і порожній масив:
{
"data": [],
"meta": {
"count": 0
}
}Наприклад:
curl "http://localhost:3000/books?q=unknown-term"Використання 200 має важливе значення:
запит сформовано правильно;
ресурс існує;
пошук виконано успішно;
просто збігів не знайдено.
Не слід повертати 404 лише тому, що пошук не знайшов результатів. Статус 404 зазвичай описує відсутність самого маршруту або конкретного ресурсу, а не порожню колекцію.
У цьому прикладі обидва запити мають однаковий результат:
GET /books
GET /books?q=Причина — порожнє значення q ігнорується:
const query = normalize(searchParams.get('q') || '');Далі перевірка:
query === ''дозволяє всім книгам пройти текстовий фільтр.
Інший допустимий підхід — вважати порожній q помилкою та повертати 400. Але таке правило потрібно явно задокументувати й реалізувати однаково для всіх клієнтів API.
Параметри URL можуть містити пробіли та спеціальні символи. Наприклад, назву:
Clean Codeпотрібно передавати в закодованому вигляді:
/books?q=Clean%20CodeУ командному рядку можна використати --data-urlencode:
curl -G "http://localhost:3000/books" \
--data-urlencode "q=Clean Code"URL.searchParams автоматично декодує значення параметра перед його використанням у програмі.
Помилковий варіант:
book.title.includes(query)У такому разі node і Node будуть різними значеннями.
Краще нормалізувати обидва рядки:
book.title.toLowerCase().includes(query.toLowerCase())У прикладі це винесено в окрему функцію normalize().
trim() для nullЯкщо параметр відсутній, searchParams.get() повертає null.
Помилковий код:
const query = searchParams.get('q').trim();Він спричинить помилку під час запиту без q.
Безпечний варіант:
const query = (searchParams.get('q') || '').trim();404 для порожнього результатуПорожній масив результатів не означає, що маршрут не існує:
{
"data": [],
"meta": {
"count": 0
}
}Для коректно виконаного пошуку використовуйте 200.
Якщо вимога передбачає пошук у назві та описі, перевірка лише title пропустить валідні результати.
Потрібно явно перевірити всі потрібні поля:
normalize(book.title).includes(query) ||
normalize(book.description).includes(query)До початку реалізації варто визначити:
які поля беруть участь у пошуку;
чи враховується регістр;
чи дозволений частковий збіг;
що відбувається з порожнім параметром;
який статус повертається, якщо результатів немає.
Без таких правил різні клієнти можуть очікувати різну поведінку одного API.
Розширте приклад такими можливостями:
Додайте параметр title, який шукає лише в назві книги.
Зробіть пошук за genre нечутливим до регістру.
Перевірте запити:
без параметрів;
з порожнім q;
із результатом;
без результатів;
з кількома параметрами одночасно.
Для кожного запиту перевірте:
HTTP-статус;
структуру JSON;
кількість елементів у data;
відповідність результатів усім переданим умовам.
Пошук у REST API реалізують через параметри запиту.
Параметр q можна застосовувати до кількох текстових полів.
Значення параметрів потрібно очищати й нормалізувати.
Фільтри комбінуються за допомогою логічного «і».
Відсутній параметр і порожнє значення мають мати визначену поведінку.
Пошук без результатів зазвичай повертає 200 і порожній масив.
Статус 404 призначений для відсутнього маршруту або конкретного ресурсу, а не для порожньої колекції.