MakeFirstStep· Frontend + ИИ

Урок 15 · Async и API

Работа с реальным API

Интернет-магазин не хранит тысячи товаров в HTML — он спрашивает их у сервера: «покажи шесть товаров, пропусти первые двенадцать, только из категории „телефоны“». Сегодня вы научитесь задавать такие вопросы через параметры запроса и превращать ответ в живой список с поиском, фильтрами и страницами — ровно так устроены каталоги, ленты и поиск на настоящих сайтах.

  • Собирать адрес запроса через URLSearchParams, не думая про пробелы и кириллицу
  • Читать документацию API и находить в ответе нужные поля
  • Рисовать список из ответа сервера, не дублируя карточки
  • Делать поиск без лишних запросов: debounce и защита от «гонки»
  • Фильтровать и сортировать — на сервере и на клиенте
  • Считать страницы по limit / skip и управлять кнопками «Назад» / «Вперёд»
Как работать с этой страницей

Код на тёмном фоне с кнопкой ▶ Запустить — живой: меняйте и запускайте. Примеры с fetch ходят в настоящие публичные API, поэтому нужен интернет. Блоки с вкладками index.html / style.css / script.js — это мини-страница: слева код, под ним предпросмотр и консоль. Задачи сами проверяются по тому, что вы выводите в консоль. Если серверу «плохо» (редкий случай), вы увидите ошибку сети — просто запустите ещё раз. Подсказки под замочком 🔒: сначала сами, потом подсказка.

В уроке три публичных API без ключей: DummyJSON (товары), Rick and Morty API (персонажи) и немного Open-Meteo (погода) в домашнем задании.

01 Параметры запроса: URL и URLSearchParams

Любой запрос к API — это адрес. Но адрес умеет не только «куда пойти», а ещё и «что именно принести». Для этого после знака ? дописывают параметры запроса (query string) в виде пар ключ=значение, разделённых &.

https://dummyjson.com/products/search?q=phone&limit=5&skip=10
Путь отвечает на вопрос «что?», параметры — «сколько, какие, в каком порядке?»
Аналогия из жизни

Заказ в чайхоне. «Курутоб» — это путь: что вы заказываете. А «без лука, порцию побольше, две пиалы чая» — это параметры: уточнения к тому же блюду. Блюдо одно, а заказы разные. Так и с API: путь /products один, а параметры limit, skip, q меняют, что именно вам принесут.

Почему нельзя склеивать строки руками

Новички собирают адрес так: "…?q=" + text + "&limit=" + limit. Пока в text одно слово латиницей, всё работает. Но стоит пользователю ввести пробел, &, # или русскую букву — адрес ломается. У URL строгие правила: такие символы нужно кодировать (пробел → + или %20, & → %26, кириллица → %D0…).

Кодировать самим — муторно и легко ошибиться. Для этого в браузере (и в Node.js) есть готовый класс URLSearchParams.

URLSearchParams собирает и кодирует · Ctrl + Enter
const params = new URLSearchParams();
params.set("q", "плов & чай");   // пробелы, &, кириллица — не наша забота
params.set("limit", 5);          // число станет строкой само
params.set("skip", 10);

console.log(params.toString());
// q=%D0%BF%D0%BB%D0%BE%D0%B2+%26+%D1%87%D0%B0%D0%B9&limit=5&skip=10

const url = "https://dummyjson.com/products/search?" + params;
console.log(url);   // в строку объект превращается автоматически

Создать URLSearchParams можно сразу из объекта или из готовой строки — удобно, чтобы разобрать чужой адрес:

Из объекта и из строки · Ctrl + Enter
const fromObject = new URLSearchParams({ q: "phone", limit: 5 });
console.log(fromObject.toString());

const fromString = new URLSearchParams("q=plov&limit=10&page=3");
console.log(fromString.get("q"));        // plov
console.log(fromString.get("limit"));    // "10" — строка, не число!
console.log(fromString.get("sort"));     // null — такого параметра нет
console.log(fromString.has("page"));     // true
set(ключ, значение)
Задать параметр. Если такой уже был — перезаписать.
append(ключ, значение)
Добавить ещё одну пару, даже если ключ уже есть (так бывает: ?tag=a&tag=b).
get(ключ) / getAll(ключ)
Прочитать значение. Всегда строка или null.
has(ключ) / delete(ключ)
Проверить наличие / убрать параметр.
toString()
Готовая строка параметров без знака ?.

Целый адрес: URL

Для разбора полного адреса есть класс URL: у него есть searchParams — тот же URLSearchParams, уже подключённый к адресу.

Можно редактировать · Ctrl + Enter
const url = new URL("https://dummyjson.com/products/search?q=phone&limit=5");

console.log(url.hostname);                  // dummyjson.com
console.log(url.pathname);                  // /products/search
console.log(url.searchParams.get("q"));     // phone

url.searchParams.set("skip", 10);           // поменяли адрес через параметры
console.log(url.href);
Частые ошибки

1. params.get("limit") + 1 даст "101", а не 11: значения — строки. Нужен Number(...). 2. Склейка строк ломается на пробелах и &. 3. Проверка if (value) при сборке адреса выбросит skip=0 — ноль «ложный», но для сервера это важное значение. Проверяйте === undefined, а не «правдивость».

Склейка строк ломает адрес · Ctrl + Enter
const text = "rock & roll";
const bad = "https://dummyjson.com/products/search?q=" + text + "&limit=5";

console.log(bad);
// сервер увидит: q="rock ", параметр " roll" без значения и limit=5 — искать будет не то
console.log(new URL(bad).searchParams.get("q"));

Читать подробнее: learn.javascript.ru — Объект URL · MDN — URLSearchParams

Практика: закрепляем

Собираем и разбираем адреса. Все задачи проверяются выводом в консоль.

ЗАДАЧА 1.1

Три параметра для поиска

лёгкая

Создайте URLSearchParams и задайте через set три параметра: q = samsa, limit = 5, skip = 10. Выведите итоговую строку параметров.

Можно редактировать · Ctrl + Enter
const params = new URLSearchParams();

// задайте q, limit и skip

// выведите params.toString()
Ожидаемый результат
q=samsa&limit=5&skip=10
Подсказка 1

params.set("q", "samsa"); — по одной строке на каждый параметр, в том порядке, в котором они в ответе.

Подсказка 2

Числа можно передавать как есть: params.set("limit", 5) — класс сам превратит их в строки.

ЗАДАЧА 1.2

Предскажи вывод: строки и числа

лёгкая

Сначала, не запуская, запишите прогноз для первых пяти строк. Запустите и сверьтесь. Потом допишите внизу: добавьте через append второе значение limit = 20 и выведите getAll("limit") через запятую (.join(",")).

Можно редактировать · Ctrl + Enter
const params = new URLSearchParams("q=plov&limit=10&page=3");

console.log(params.get("limit") + 1);
console.log(Number(params.get("limit")) + 1);
console.log(params.get("sort"));
console.log(params.has("page"));
console.log(params.get("page") * 2);

// ваша часть: append + getAll
Ожидаемый результат
101
11
null
true
6
10,20
Подсказка 1

Плюс со строкой — склейка: "10" + 1 это "101". А вот умножение само превращает строку в число.

Подсказка 2

params.append("limit", 20); и console.log(params.getAll("limit").join(","));

ЗАДАЧА 1.3

Функция buildUrl

средняя

Напишите buildUrl(base, params): она принимает адрес и объект параметров и возвращает готовый адрес. Параметры со значением undefined, null или пустой строкой пропускаются; 0 — не пропускается. Если параметров не осталось, знак ? не добавляется.

Можно редактировать · Ctrl + Enter
function buildUrl(base, params) {
  // ваш код
}

console.log(buildUrl("https://dummyjson.com/products/search", { q: "phone", limit: 5, skip: 0 }));
console.log(buildUrl("https://dummyjson.com/products", { limit: 10, skip: undefined, q: "" }));
console.log(buildUrl("https://dummyjson.com/products", {}));
Ожидаемый результат
https://dummyjson.com/products/search?q=phone&limit=5&skip=0
https://dummyjson.com/products?limit=10
https://dummyjson.com/products
Подсказка 1

Пройдите по Object.entries(params) циклом for…of, пропускайте плохие значения через continue, а хорошие кладите в URLSearchParams.

Подсказка 2

Условие пропуска: value === undefined || value === null || value === "". В конце: const text = query.toString(); return text ? `${base}?${text}` : base;

ЗАДАЧА 1.4

Найди баг: поиск «rock & roll»

лёгкая

Пользователь искал rock & roll, а сервер получил что-то странное. Перепишите сборку адреса через URLSearchParams (параметры q и limit=5). Выведите адрес, а второй строкой — что вернёт new URL(url).searchParams.get("q"): это то, что увидит сервер.

Можно редактировать · Ctrl + Enter
const query = "rock & roll";
const url = "https://dummyjson.com/products/search?q=" + query + "&limit=5";

console.log(url);
console.log(new URL(url).searchParams.get("q"));
Ожидаемый результат
https://dummyjson.com/products/search?q=rock+%26+roll&limit=5
rock & roll
Подсказка 1

Создайте new URLSearchParams({ q: query, limit: 5 }).

Подсказка 2

const url = "https://dummyjson.com/products/search?" + params; — объект сам превратится в строку.

02 Читаем документацию и ответ API

У каждого API свои правила: как называются параметры, как устроен ответ, что будет, если ничего не найдено. Узнать это можно только из документации и из самого ответа. Привычка №1 разработчика: сначала посмотреть, что реально приходит, и только потом писать код.

Из урока 14 вы помните обёртку над fetch. Она нам понадобится в каждом примере, поэтому вот её короткая версия — в уроке она будет лежать в начале каждого блока:

Как выглядит ответ DummyJSON · Ctrl + Enter
async function getJson(url) {
  const response = await fetch(url);
  if (!response.ok) throw new Error("HTTP " + response.status);
  return response.json();
}

async function main() {
  const data = await getJson("https://dummyjson.com/products?limit=2&select=title,price");
  console.log(Object.keys(data));
  console.log(data.products);
  console.log("всего товаров на сервере больше, чем пришло:", data.total > data.products.length);
}
main();

Видите: ответ — не просто массив товаров, а объект-конверт. Сами товары лежат в products, а рядом — служебные поля total (сколько всего), skip и limit (какой кусок вернули). Они нам понадобятся для пагинации.

DummyJSONRick and Morty APIJSONPlaceholder
Где списокdata.productsdata.resultsсам ответ — массив
Поиск/products/search?q=?name=нет, только фильтры по полям: ?userId=1
Страницыlimit + skippage (по 20 штук)_page + _limit
Сколько всегоdata.totaldata.info.count, info.pagesзаголовок x-total-count
Ничего не нашли200 и пустой products404 и { error }200 и пустой массив

Последняя строка — самая коварная: «ничего не найдено» в одном API это пустой список, а в другом — ошибка 404. Если не знать, поиск по Рику и Морти будет «падать» на любой опечатке.

404 — это не всегда поломка · Ctrl + Enter
async function main() {
  const found = await fetch("https://rickandmortyapi.com/api/character?name=rick");
  console.log("rick →", found.status);

  const none = await fetch("https://rickandmortyapi.com/api/character?name=zzzzqq");
  console.log("zzzzqq →", none.status, none.ok);
  console.log(await none.json());
}
main();
Аналогия из жизни

Документация API — это меню чайханы. Вы не угадываете, что есть на кухне: открываете меню и видите блюда (эндпоинты), порции и добавки (параметры). А ответ сервера — само блюдо на тарелке: нужно посмотреть, что в нём лежит, прежде чем начать есть.

Как читать документацию за 2 минуты

  1. Найдите нужный эндпоинт. «Мне нужен поиск товаров» → GET /products/search.
  2. Выпишите параметры: какие обязательные, какие нет, какие значения допустимы (limit, skip, select, sortBy, order).
  3. Откройте пример ответа и найдите, где лежит список и где служебные поля.
  4. Проверьте руками: вставьте адрес в браузер или в console.log и убедитесь, что ответ такой, как в документации.
  5. Узнайте про «пусто» и «ошибка»: какой статус и какое тело придёт, если ничего нет.

Ещё один приём: параметр select у DummyJSON оставляет в ответе только нужные поля. Запрос становится легче и быстрее — зачем качать описание и отзывы, если нужны название и цена?

Практика: закрепляем

Три задачи, в которых вы читаете ответ API и учитываете его особенности.

ЗАДАЧА 2.1

Что внутри конверта

лёгкая

Запросите три товара (limit=3, select=title,price). Выведите ключи ответа через запятую (Object.keys(data).join(", ")), а второй строкой — сколько товаров в data.products.

Можно редактировать · Ctrl + Enter
async function main() {
  const url = "https://dummyjson.com/products?limit=3&select=title,price";
  // fetch → response.json() → два console.log
}
main();
Ожидаемый результат
products, total, skip, limit
3
Подсказка 1

Два await подряд: const response = await fetch(url); const data = await response.json();

Подсказка 2

Список — data.products, у массива есть .length.

ЗАДАЧА 2.2

Найди баг: «data.map is not a function»

лёгкая

Функция должна вывести Загружено: 4, но падает с ошибкой. Запустите, прочитайте ошибку, исправьте.

Этот код выдаст ошибку · Ctrl + Enter
async function loadTitles() {
  const response = await fetch("https://dummyjson.com/products?limit=4&select=title");
  const data = await response.json();
  const titles = data.map((p) => p.title);
  console.log("Загружено:", titles.length);
}
loadTitles();
Ожидаемый результат
Загружено: 4
Подсказка 1

data — не массив, а объект-конверт. Какое его поле содержит массив товаров?

Подсказка 2

Замените data.map(...) на data.products.map(...).

ЗАДАЧА 2.3

Поиск персонажей без падений

средняя

Допишите searchCharacters(name): она запрашивает https://rickandmortyapi.com/api/character?name=… (параметр собирайте через URLSearchParams) и возвращает массив data.results. Если сервер ответил 404 — это «ничего не найдено», верните пустой массив []. Любую другую неудачу (!response.ok) считайте ошибкой и бросайте Error.

Можно редактировать · Ctrl + Enter
async function searchCharacters(name) {
  // ваш код
}

async function main() {
  const found = await searchCharacters("rick");
  console.log(found.length > 0);
  const none = await searchCharacters("zzzzqq");
  console.log(none.length);
}
main();
Ожидаемый результат
true
0
Подсказка 1

Сначала const response = await fetch(...), потом проверки статуса — и только после них response.json().

Подсказка 2

if (response.status === 404) return []; затем if (!response.ok) throw new Error("HTTP " + response.status);

03 Рендер списка из API

Данные пришли — теперь покажем их на странице. Схема всегда одна: fetch → массив → для каждого элемента создать узел → вставить в страницу. Это то, что вы делали в уроках 8 и 11, только массив теперь приходит не из переменной, а из интернета.

  1. Запросили данные: await fetch(url), await response.json().
  2. Достали список из конверта: data.products.
  3. Очистили контейнер: list.replaceChildren().
  4. Для каждого товара создали <li> / карточку, заполнили textContent и добавили в контейнер.
Список товаров из API · Ctrl + Enter
<h3>Первые 5 товаров</h3>
<ul id="list"></ul>
Предпросмотр
Нажмите «Запустить», чтобы увидеть результат.
Аналогия из жизни

Рендер — это витрина на бозоре. Каждое утро продавец не дописывает товары поверх вчерашних, а выкладывает витрину заново: убрал старое — выставил актуальное. Функция render должна работать так же: сначала очистить, потом построить. Тогда её можно вызывать сколько угодно раз (при поиске, при смене страницы), и ничего не задвоится.

Правило: render полностью перестраивает список

Самая частая ошибка новичков — забыть очистку. Первый вызов работает, а при втором (новая страница, новый поиск) карточки добавляются к старым. Поэтому всё, что рисует список, начинается с replaceChildren() (или innerHTML = "").

Данные с сервера — не доверяйте им

Если вставлять ответ через innerHTML, то любой «товар» с названием <b>…</b> превратится в разметку, а злоумышленник сможет подсунуть и скрипт. Для текста из API используйте textContent: он вставляет строку как есть, без разбора тегов.

textContent против innerHTML · Ctrl + Enter
const title = "<b>СКИДКА!</b> Самбуса";   // как будто пришло с сервера

const safe = document.createElement("p");
safe.textContent = title;
const risky = document.createElement("p");
risky.innerHTML = title;

console.log(safe.innerHTML);     // теги превращены в безопасный текст
console.log(risky.children.length);  // 1 — внутри появился настоящий <b>

Карточки с картинкой

Настоящий каталог — это не список строк, а карточки. Принцип тот же, узлов просто больше. Картинку берём из поля thumbnail и обязательно задаём alt — для доступности и на случай, если картинка не загрузится.

Карточки · Ctrl + Enter
<div id="grid" class="grid"></div>
Предпросмотр
Нажмите «Запустить», чтобы увидеть результат.
Пустой список — тоже состояние

Если сервер вернул ноль товаров, пользователь должен увидеть «Ничего не найдено», а не пустое место. Про состояния загрузки и ошибок мы подробно поговорим на следующем уроке, а пока заложим минимум: render умеет показывать пустой результат.

Читать подробнее: learn.javascript.ru — Fetch · MDN — Element.replaceChildren

Практика: закрепляем

Четыре задачи на рендер. Результат — в предпросмотре и в консоли под песочницей.

ЗАДАЧА 3.1

Пять товаров в список

лёгкая

Допишите loadProducts: для каждого товара создайте <li> с текстом название — цена $ (тире с пробелами вокруг) и добавьте в list. Консоль уже выведет проверку: сколько элементов в списке и есть ли в первом тире.

Список товаров · Ctrl + Enter
<ul id="list"></ul>
Предпросмотр
Нажмите «Запустить», чтобы увидеть результат.
Ожидаемый результат
5
true
Подсказка 1

for (const product of data.products) { ... } — внутри цикла создаём один li.

Подсказка 2

const li = document.createElement("li"); li.textContent = `${product.title} — ${product.price} $`; list.append(li);

ЗАДАЧА 3.2

Карточки с картинками

средняя

Выведите шесть товаров карточками: <article class="card"> с <img> (src = thumbnail, alt = название), заголовком <h3> и ценой <p>. Всё через createElement и textContent.

Карточки товаров · Ctrl + Enter
<div id="grid" class="grid"></div>
Предпросмотр
Нажмите «Запустить», чтобы увидеть результат.
Ожидаемый результат
6
6
Подсказка 1

Создайте четыре узла: article, img, h3, p. Классу card соответствует card.className = "card".

Подсказка 2

Соберите карточку одной строкой: card.append(img, title, price); и вставьте в grid.

ЗАДАЧА 3.3

Найди баг: задвоенный список

лёгкая

Список обновляют дважды (как будто пользователь нажал «обновить»). Должно остаться 3 элемента, а их 6. Исправьте loadProducts, не трогая проверку.

Двойное обновление · Ctrl + Enter
<ul id="list"></ul>
Предпросмотр
Нажмите «Запустить», чтобы увидеть результат.
Ожидаемый результат
3
Подсказка 1

Каждый вызов добавляет новые li к тем, что уже есть. Чего не хватает перед циклом?

Подсказка 2

Добавьте list.replaceChildren(); после получения данных, до цикла.

ЗАДАЧА 3.4

renderList с пустым состоянием

сложная

Напишите renderList(items) (items — массив строк). Она всегда сначала очищает list. Для пустого массива добавляет один <li class="empty">Ничего не найдено</li>, для непустого — по <li> на строку. Сеть не нужна.

renderList · Ctrl + Enter
<ul id="list"></ul>
Предпросмотр
Нажмите «Запустить», чтобы увидеть результат.
Ожидаемый результат
2
1 Ничего не найдено
Чой
Подсказка 1

Первой строкой — list.replaceChildren();. Затем ветка if (items.length === 0) { ...; return; }.

Подсказка 2

В пустой ветке создайте li, задайте className = "empty" и текст, добавьте в список и сделайте return. Ниже, для непустого, обычный цикл for (const item of items).

05 Фильтрация и сортировка

Фильтровать можно в двух местах — и от выбора зависит скорость и удобство:

Фильтр на сервере

Вы просите у API только нужное: /products/category/smartphones. Приходит меньше данных, работает на больших каталогах (тысячи записей), совместимо с пагинацией. Но каждый новый фильтр — новый запрос.

Фильтр на клиенте

Данные уже загружены, а вы просто делаете filter по массиву. Мгновенно, без сети. Но работает, только если на руках весь набор — для огромных каталогов и страничной загрузки не подходит.

Аналогия из жизни

Сервер — это склад на «Корвоне»: вы звоните и просите «пришлите только красные ковры» — вам везут нужное. Клиент — это ваш прилавок: ковры уже привезли, вы просто раскладываете красные влево, остальные вправо. Склад экономит дорогу, прилавок — время ожидания.

Фильтр по категории — на стороне сервера

Категории и товары категории · Ctrl + Enter
async function getJson(url) {
  const response = await fetch(url);
  return response.json();
}

async function main() {
  const categories = await getJson("https://dummyjson.com/products/category-list");
  console.log("категорий больше десяти:", categories.length > 10);
  console.log(categories.slice(0, 3));

  const data = await getJson("https://dummyjson.com/products/category/smartphones?limit=3&select=title,category");
  data.products.forEach((p) => console.log(p.category, "—", p.title));
}
main();

Сортировка на сервере

DummyJSON умеет сортировать сам: параметры sortBy (по какому полю) и order (asc — по возрастанию, desc — по убыванию).

Самые дешёвые товары · Ctrl + Enter
async function main() {
  const params = new URLSearchParams({ sortBy: "price", order: "asc", limit: 5, select: "title,price" });
  const response = await fetch("https://dummyjson.com/products?" + params);
  const data = await response.json();
  data.products.forEach((p) => console.log(p.price, p.title));
}
main();
Ограничения API — читайте документацию

У DummyJSON эндпоинт /products/search не комбинируется с категорией: вы либо ищете по словам, либо берёте товары категории. Придумывать ?q=phone&category=smartphones бесполезно — сервер проигнорирует категорию. Реальные API полны таких ограничений. В нашем приложении правило будет простым: если введён поиск — работает поиск; если выбрана категория — работает категория; выбирая одно, сбрасываем другое.

Фильтр на клиенте: filter + includes

Когда нужно скомбинировать условия, а данных мало, фильтруем массив на месте. Одно условие — один «отсев»:

Несколько условий в одном filter · Ctrl + Enter
const menu = [
  { title: "Самбуса", category: "выпечка", price: 6 },
  { title: "Курутоб", category: "горячее", price: 35 },
  { title: "Шакароб", category: "салаты", price: 15 },
  { title: "Оши палав", category: "горячее", price: 40 },
];

function applyFilters(items, { q = "", category = "", maxPrice = Infinity }) {
  const text = q.trim().toLowerCase();
  return items.filter((item) => {
    if (text && !item.title.toLowerCase().includes(text)) return false;  // поиск без учёта регистра
    if (category && item.category !== category) return false;
    return item.price <= maxPrice;
  });
}

console.log(applyFilters(menu, { category: "горячее" }).map((i) => i.title));
console.log(applyFilters(menu, { q: "ША", maxPrice: 20 }).map((i) => i.title));

Обратите внимание на приём: фильтры «выключаются» значениями по умолчанию (q = "", category = "", maxPrice = Infinity) — пустое условие пропускает всё.

Выпадающий список из данных

Категории не нужно вписывать руками: их можно собрать из данных. new Set(...) оставляет только уникальные значения — то, что нужно для <select>:

Можно редактировать · Ctrl + Enter
const menu = ["выпечка", "горячее", "горячее", "салаты", "выпечка"];
const unique = [...new Set(menu)];
console.log(unique);   // ["выпечка", "горячее", "салаты"]

Читать подробнее: learn.javascript.ru — Методы массивов · MDN — Array.prototype.filter

Практика: закрепляем

Фильтры и сортировка — на сервере и на клиенте.

ЗАДАЧА 5.1

Товары одной категории

лёгкая

Выведите три строки: есть ли "smartphones" в category-list; все ли товары из ответа /products/category/smartphones?limit=3&select=category имеют категорию smartphones (every); сколько товаров пришло.

Можно редактировать · Ctrl + Enter
async function getJson(url) {
  const response = await fetch(url);
  return response.json();
}

async function main() {
  // три console.log
}
main();
Ожидаемый результат
true
true
3
Подсказка 1

const categories = await getJson(".../products/category-list"); — это обычный массив строк, у него есть includes.

Подсказка 2

data.products.every((p) => p.category === "smartphones")

ЗАДАЧА 5.2

Самые дорогие: сортировка и проверка

средняя

Запросите 5 товаров, отсортированных по цене по убыванию (sortBy=price, order=desc, select=price, limit=5 — соберите через URLSearchParams). Проверьте, что цены действительно убывают, и выведите результат и количество.

Можно редактировать · Ctrl + Enter
async function main() {
  // запрос → массив цен → проверка через every
  // console.log("по убыванию:", ...);
  // console.log("количество:", ...);
}
main();
Ожидаемый результат
по убыванию: true
количество: 5
Подсказка 1

Цены: data.products.map((p) => p.price).

Подсказка 2

Каждая цена не больше предыдущей: prices.every((price, i) => i === 0 || prices[i - 1] >= price)

ЗАДАЧА 5.3

Меню: фильтр по трём условиям

средняя

Напишите applyFilters(items, { q, category, maxPrice }). Поиск — по названию, без учёта регистра; категория — точное совпадение; maxPrice — «не дороже». Любое условие можно не передавать. Функция show уже написана.

Можно редактировать · Ctrl + Enter
const menu = [
  { title: "Самбуса с мясом", category: "выпечка", price: 6 },
  { title: "Самбуса с тыквой", category: "выпечка", price: 5 },
  { title: "Курутоб", category: "горячее", price: 35 },
  { title: "Шакароб", category: "салаты", price: 15 },
  { title: "Оши палав", category: "горячее", price: 40 },
  { title: "Манту", category: "горячее", price: 30 },
];

function applyFilters(items, { q = "", category = "", maxPrice = Infinity }) {
  // ваш код
}

function show(items) {
  console.log(items.map((item) => item.title).join(", ") || "ничего не найдено");
}

show(applyFilters(menu, { q: "самбуса" }));
show(applyFilters(menu, { category: "горячее", maxPrice: 35 }));
show(applyFilters(menu, { q: "ШАК" }));
show(applyFilters(menu, { category: "супы" }));
show(applyFilters(menu, { q: "а", category: "горячее", maxPrice: 30 }));
Ожидаемый результат
Самбуса с мясом, Самбуса с тыквой
Курутоб, Манту
Шакароб
ничего не найдено
Манту
Подсказка 1

Приведите искомый текст и название к нижнему регистру: toLowerCase(). Проверка вхождения — includes.

Подсказка 2

Внутри items.filter(...) по очереди отсеивайте: if (text && !… ) return false;, if (category && … !== category) return false; и в конце return item.price <= maxPrice;

ЗАДАЧА 5.4

Select с категориями из данных

сложная

Дополните песочницу: 1) заполните select: первая опция «Все» со значением "", потом по одной опции на каждую уникальную категорию (помогут new Set и готовая addOption); 2) при событии change показывайте только товары выбранной категории (или все, если выбрано «Все»), используя готовую renderList. Ниже код сам «выбирает» категории и печатает проверку.

Фильтр по категории · Ctrl + Enter
<select id="category"></select>
<ul id="list"></ul>
Предпросмотр
Нажмите «Запустить», чтобы увидеть результат.
Ожидаемый результат
опций: 4
горячее: 3
все: 6
Подсказка 1

addOption("", "Все");, затем цикл for (const category of new Set(menu.map((item) => item.category))).

Подсказка 2

В обработчике: const value = select.value; renderList(value ? menu.filter((item) => item.category === value) : menu); Не забудьте один вызов renderList(menu) до подписки.

06 Пагинация: страницы по limit и skip

В каталоге 194 товара — грузить их все сразу глупо: долго, тяжело и никто столько не пролистает. Поэтому сервер отдаёт данные порциями, а вы просите нужную порцию. Это и есть пагинация (постраничная загрузка).

Аналогия из жизни

Плов на свадьбе подают не всем котлом, а порциями на ляганах: на каждый — десять порций. Хотите добавки — просите следующий ляган. Параметр limit — сколько порций на ляган, skip — сколько порций вы уже съели и пропускаете.

Два популярных способа

СпособПараметрыКто использует
limit + skip (offset)«дай limit штук, пропустив первые skip»DummyJSON, PokeAPI (limit + offset)
page«дай страницу № page», размер страницы задан серверомRick and Morty (20 на странице), CoinGecko (page + per_page)

Пользователь мыслит номерами страниц: «страница 3». Серверу DummyJSON нужен skip. Переводим формулой:

skip=(page − 1)×limit
Страница 1 пропускает 0 записей, страница 2 — limit записей, страница 3 — 2 × limit
Страницаskip при limit = 10Какие записи
101–10
21011–20
32021–30

А сколько всего страниц? Делим общее число записей (total из ответа) на размер страницы и округляем вверх — неполная последняя страница тоже страница:

Формулы пагинации · Ctrl + Enter
const pagesCount = (total, limit) => Math.ceil(total / limit);
const pageToSkip = (page, limit) => (page - 1) * limit;

console.log(pagesCount(100, 10));   // 10
console.log(pagesCount(101, 10));   // 11 — одна запись «спилась» на новую страницу
console.log(pageToSkip(3, 10));     // 20
Живая страница из API · Ctrl + Enter
async function loadPage(page, limit) {
  const params = new URLSearchParams({ limit, skip: (page - 1) * limit, select: "title" });
  const response = await fetch("https://dummyjson.com/products?" + params);
  return response.json();
}

async function main() {
  const data = await loadPage(2, 5);
  console.log(`skip=${data.skip}, limit=${data.limit}`);
  console.log("страниц всего больше одной:", Math.ceil(data.total / data.limit) > 1);
  data.products.forEach((p) => console.log(p.id, p.title));
}
main();

Кнопки «Назад» и «Вперёд»

Состояние пагинации — это всего два числа: текущая страница и общее число страниц. Всё остальное рисуется от них:

  • «Назад» выключена на первой странице: prev.disabled = page === 1.
  • «Вперёд» выключена на последней: next.disabled = page === pages.
  • Подпись «Страница 3 из 5» собирается из тех же чисел.
  • После нового поиска или смены категории страница сбрасывается на первую — иначе можно оказаться на 7-й странице результатов, которых всего три.
Частые ошибки

1. skip = page * limit без «−1» — пропускается целая страница, первая порция товаров не видна никогда. 2. Страницы считают через Math.floor или Math.round — теряется последняя страница. 3. Забывают сбросить page = 1 при новом поиске. 4. Пустой поиск у DummyJSON вернёт total: 0 — делить на ноль не страшно, но «страница 1 из 0» выглядит странно: показывайте минимум одну страницу (Math.max(1, …)).

Rick and Morty: страницы без skip

Там всё проще: ?page=2. А сколько страниц — подсказывает сам сервер в info.pages, и даже даёт info.next — адрес следующей страницы или null, если страница последняя. Это удобно для кнопки «Показать ещё».

Показать ещё: накапливаем страницы · Ctrl + Enter
let page = 0;
let items = [];

async function loadMore(name) {
  page++;
  const params = new URLSearchParams({ name, page });
  const response = await fetch("https://rickandmortyapi.com/api/character?" + params);
  const data = await response.json();
  items = items.concat(data.results);       // не заменяем, а дописываем
  return data.info.next !== null;           // есть ли ещё страницы
}

async function main() {
  let hasMore = await loadMore("rick");
  console.log("после 1-й загрузки:", items.length, hasMore);
  hasMore = await loadMore("rick");
  console.log("после 2-й загрузки:", items.length, hasMore);
}
main();

Есть два интерфейса: страницы (кнопки, пролистываем номерами; render заменяет список) и «Показать ещё» / бесконечная лента (карточки дописываются; render добавляет, а не очищает). Выбор — дело дизайна, а запросы одинаковые.

Читать подробнее: DummyJSON — Products · Rick and Morty API — Character · MDN — Math.ceil

Практика: закрепляем

Формулы, живые страницы и кнопки.

ЗАДАЧА 6.1

Сколько страниц

лёгкая

Напишите pagesCount(total, limit) — количество страниц с учётом неполной последней.

Можно редактировать · Ctrl + Enter
function pagesCount(total, limit) {
  // ваш код
}

console.log(pagesCount(100, 10));
console.log(pagesCount(101, 10));
console.log(pagesCount(0, 10));
console.log(pagesCount(5, 10));
Ожидаемый результат
10
11
0
1
Подсказка 1

Деление даёт дробь для неполной страницы: 101 / 10 = 10.1. Нужно округлить вверх.

Подсказка 2

return Math.ceil(total / limit);

ЗАДАЧА 6.2

Адрес страницы

лёгкая

Напишите pageToSkip(page, limit) и productsUrl(page, limit): адрес вида https://dummyjson.com/products?limit=10&skip=20 (сначала limit, потом skip).

Можно редактировать · Ctrl + Enter
function pageToSkip(page, limit) {
  // ваш код
}

function productsUrl(page, limit) {
  // ваш код
}

console.log(productsUrl(1, 10));
console.log(productsUrl(3, 10));
console.log(productsUrl(2, 5));
Ожидаемый результат
https://dummyjson.com/products?limit=10&skip=0
https://dummyjson.com/products?limit=10&skip=20
https://dummyjson.com/products?limit=5&skip=5
Подсказка 1

Первая страница пропускает 0 записей: (page - 1) * limit.

Подсказка 2

Шаблонная строка: `https://dummyjson.com/products?limit=${limit}&skip=${pageToSkip(page, limit)}`

ЗАДАЧА 6.3

Две страницы без повторов

средняя

Напишите loadPage(page, limit) (запрос к /products с limit, skip и select=title, возвращает весь объект ответа). Загрузите страницы 1 и 2 по 5 штук и выведите: skip и limit второй страницы, нет ли в ней товаров первой (по id), и больше ли страниц одной.

Можно редактировать · Ctrl + Enter
async function loadPage(page, limit) {
  // ваш код
}

async function main() {
  const first = await loadPage(1, 5);
  const second = await loadPage(2, 5);
  console.log(`skip=${second.skip}, limit=${second.limit}`);
  // без повторов: используйте Set из id первой страницы
  // страниц больше одной: Math.ceil(second.total / second.limit) > 1
}
main();
Ожидаемый результат
skip=5, limit=5
без повторов: true
страниц больше одной: true
Подсказка 1

const ids = new Set(first.products.map((p) => p.id));

Подсказка 2

second.products.every((p) => !ids.has(p.id)) — ни один id второй страницы не лежит в ids.

ЗАДАЧА 6.4

Кнопки пагинации

средняя

В песочнице 47 записей по 10 на странице — 5 страниц. Напишите render (подпись «Страница N из M», выключение кнопок) и подпишите кнопки на click (page-- / page++ и render()). Не забудьте вызвать render один раз при старте. Код ниже «кликает» по кнопкам, в том числе по выключенным.

Пагинация · Ctrl + Enter
<button id="prev">← Назад</button>
<span id="label"></span>
<button id="next">Вперёд →</button>
Предпросмотр
Нажмите «Запустить», чтобы увидеть результат.
Ожидаемый результат
Страница 1 из 5 | назад: выкл | вперёд: вкл
Страница 3 из 5 | назад: вкл | вперёд: вкл
Страница 5 из 5 | назад: вкл | вперёд: выкл
Страница 4 из 5 | назад: вкл | вперёд: вкл
Подсказка 1

label.textContent = `Страница ${page} из ${pages}`; и prev.disabled = page === 1;

Подсказка 2

Клик по уже выключенной кнопке ничего не делает, поэтому «перелистать» дальше пятой страницы не получится: следите лишь за тем, чтобы render() вызывался после каждого изменения page.

ЗАДАЧА 6.5

«Показать ещё» на Рике и Морти

сложная

Напишите loadMore(name): она увеличивает счётчик page, запрашивает https://rickandmortyapi.com/api/character?name=…&page=…, дописывает data.results в массив items и возвращает true, если есть следующая страница (data.info.next !== null). Вызовите её дважды подряд для «rick» и выведите после каждого вызова: сколько накопилось и есть ли ещё.

Можно редактировать · Ctrl + Enter
let page = 0;
let items = [];

async function loadMore(name) {
  // ваш код
}

async function main() {
  // два вызова loadMore, после каждого console.log(items.length, hasMore)
}
main();
Ожидаемый результат
20 true
40 true
Подсказка 1

Страница размером 20 — задана сервером. Накопление: items = items.concat(data.results); (а не items = data.results).

Подсказка 2

В main: let hasMore = await loadMore("rick"); console.log(items.length, hasMore); — и ещё раз то же самое.

07 ИИ-блок: читаем документацию API вместе с ИИ

Документации бывают длинные, на английском и написаны так, что нужный параметр найти сложно. ИИ отлично помогает быстро найти эндпоинт и параметры — если дать ему сам текст документации. Но у него есть слабость: когда документации под рукой нет, он выдумывает параметры, которые звучат правдоподобно, но у вашего API не существуют.

Правила курса, которые здесь особенно важны

1. Сначала 15 минут пробуете сами: открываете документацию, ищете эндпоинт, пишете адрес вручную. 2. Потом идёте к ИИ. 3. Не вставляйте в проект код, который не можете объяснить строчка за строчкой. 4. Всё, что сказал ИИ про параметры API, сверяйте с настоящим ответом сервера — запросом в консоли.

Как просить: плохо и хорошо

Формула промпта курса: контекст → задача → ограничения → формат ответа → проверка. Главное для документации — вставить её текст (или нужный кусок), а не просить «вспомнить».

как в dummyjson искать товары по категории и по названию одновременно
Я учу JavaScript, пишу каталог товаров на чистом JS без библиотек. Вот фрагмент документации DummyJSON: [вставьте текст со страницы https://dummyjson.com/docs/products]. Мне нужно: показать страницу из 6 товаров, искать по названию и фильтровать по категории. Скажи: какой эндпоинт для поиска, какой для категории, какие у них параметры (limit, skip, select, sortBy, order) и можно ли их комбинировать. Ответь таблицей «задача → эндпоинт → параметры» и одним примером запроса на fetch с URLSearchParams. Если в документации чего-то нет, так и скажи, ничего не выдумывай. В конце напиши, что мне проверить руками в консоли.

Скопируйте хороший промпт, подставьте настоящий текст документации — и получите ответ. А теперь проверим главную ловушку: что будет, если поверить выдуманному параметру. Допустим, ИИ уверенно посоветовал ?category=smartphones к поиску:

Проверяем совет ИИ запросом · Ctrl + Enter
async function main() {
  const params = new URLSearchParams({ q: "phone", category: "smartphones", limit: 10, select: "title,category" });
  const response = await fetch("https://dummyjson.com/products/search?" + params);
  const data = await response.json();

  const works = data.products.every((p) => p.category === "smartphones");
  console.log("параметр category сработал:", works);
  console.log(data.products.map((p) => p.category));
}
main();
// false — сервер молча проигнорировал выдуманный параметр. Ошибки нет, но и фильтра нет!
Что проверять в ответе ИИ по API

1) Такой параметр есть в документации? 2) Список лежит там, где ИИ сказал (data.products или просто массив)? 3) Что вернётся при «ничего не найдено» — пустой список или 404? 4) Нужен ли ключ API (в нашем курсе — нет)? 5) Совпадает ли пример с реальным ответом в консоли?

Так выглядит кусок документации. Научитесь читать его без перевода — по названиям параметров и примерам:

Фрагмент документации (сокращённо)
GET https://dummyjson.com/products/category/{category}

Query-параметры:
  limit    сколько записей вернуть
  skip     сколько записей пропустить
  select   поля через запятую (title,price)
  sortBy   поле для сортировки
  order    asc | desc

Ответ: { products: [...], total, skip, limit }

Практика: проверяем ИИ и читаем документацию

ЗАДАЧА 7.1

ИИ написал сборку адреса — найдите три бага

средняя

ИИ написал функцию для адреса списка товаров. Она выглядит нормально, но в ней три ошибки: неверный skip, поиск идёт не на тот эндпоинт и значение q не кодируется. Исправьте (параметры в адресе — в порядке limit, skip, q).

Можно редактировать · Ctrl + Enter
// написано ИИ — не доверяем, проверяем
function buildProductsUrl(page, limit, q) {
  const skip = page * limit;
  let url = "https://dummyjson.com/products?limit=" + limit + "&skip=" + skip;
  if (q) url += "&q=" + q;
  return url;
}

console.log(buildProductsUrl(1, 10, ""));
console.log(buildProductsUrl(3, 10, "iphone case"));
Ожидаемый результат
https://dummyjson.com/products?limit=10&skip=0
https://dummyjson.com/products/search?limit=10&skip=20&q=iphone+case
Подсказка 1

Страница 1 должна пропускать 0 записей: (page - 1) * limit. А для непустого q нужен путь /products/search.

Подсказка 2

Соберите new URLSearchParams({ limit, skip }), при непустом q добавьте params.set("q", q) и верните адрес с /search; иначе — без него.

ЗАДАЧА 7.2

Адрес по документации

средняя

По фрагменту выше: три самых дешёвых ноутбука — категория laptops, limit=3, сортировка по цене по возрастанию, поля title,price (параметры в таком порядке: limit, sortBy, order, select). Выведите адрес, затем количество полученных товаров и то, что цены действительно идут по возрастанию. (Запятая в адресе превратится в %2C — это нормально, сервер понимает.)

Можно редактировать · Ctrl + Enter
async function main() {
  // 1) соберите адрес через URLSearchParams и выведите его
  // 2) запросите, выведите количество и проверку сортировки
}
main();
Ожидаемый результат
https://dummyjson.com/products/category/laptops?limit=3&sortBy=price&order=asc&select=title%2Cprice
3
true
Подсказка 1

new URLSearchParams({ limit: 3, sortBy: "price", order: "asc", select: "title,price" }) — порядок ключей в объекте задаёт порядок в адресе.

Подсказка 2

Проверка сортировки: prices.every((price, i) => i === 0 || prices[i - 1] <= price).

08 Практика на уроке: каталог с поиском, категориями и страницами

Соберём приложение из всех кусочков урока. Это тот самый «рабочий каталог»: поиск, фильтр по категории, постраничная загрузка и данные с настоящего сервера. Строим шаг за шагом — после каждого шага запускайте и смотрите.

Шаг 1. Состояние и адрес запроса

Вся логика приложения держится на одном объекте state (урок 11): что ищем, какая категория, какая страница. А функция buildUrl превращает состояние в адрес. Запустите — пока без сети:

Шаг 1. state → адрес · Ctrl + Enter
const API = "https://dummyjson.com/products";
const LIMIT = 6;
const state = { q: "", category: "", page: 1 };

function buildUrl() {
  const params = new URLSearchParams({
    limit: LIMIT,
    skip: (state.page - 1) * LIMIT,
    select: "title,price,thumbnail",
  });
  if (state.q) {                                  // поиск главнее категории
    params.set("q", state.q);
    return `${API}/search?${params}`;
  }
  if (state.category) return `${API}/category/${state.category}?${params}`;
  return `${API}?${params}`;
}

console.log(buildUrl());

state.page = 3;
console.log(buildUrl());

state.category = "laptops";
state.page = 1;
console.log(buildUrl());

state.q = "mac";
console.log(buildUrl());

Шаг 2. Загрузка и рендер

Подключаем fetch и render: запрос по buildUrl(), очистка списка, карточки, а для пустого результата — сообщение. Состояние «Загрузка…» показываем пока ждём.

Шаг 2. Список из API · Ctrl + Enter
<p id="status"></p>
<ul id="list" class="grid"></ul>
Предпросмотр
Нажмите «Запустить», чтобы увидеть результат.

Шаг 3. Страницы

К state добавляем pages (сколько страниц) — его считаем из data.total после каждого ответа. Функция go(page) проверяет границы, меняет страницу и загружает. updatePager рисует подпись и выключает лишние кнопки.

Шаг 3. Что добавляем к шагу 2
const state = { q: "", category: "", page: 1, pages: 1 };

function updatePager() {
  info.textContent = `Страница ${state.page} из ${state.pages}`;
  prev.disabled = state.page <= 1;
  next.disabled = state.page >= state.pages;
}

async function load() {
  statusEl.textContent = "Загрузка…";
  const response = await fetch(buildUrl());
  const data = await response.json();
  state.pages = Math.max(1, Math.ceil(data.total / LIMIT));   // минимум одна страница
  render(data.products);
  updatePager();
}

async function go(page) {
  if (page < 1 || page > state.pages) return;   // за границы не ходим
  state.page = page;
  await load();
}

prev.addEventListener("click", () => go(state.page - 1));
next.addEventListener("click", () => go(state.page + 1));

Шаг 4. Поиск и категории

Поиск — это debounce + функция search, которая задаёт state.q, сбрасывает категорию и страницу и загружает. Категория — симметрично. В load добавляем защиту от гонки (lastRequest) и минимальную обработку ошибок. Вот готовое приложение целиком — поищите «phone», выберите категорию, полистайте страницы:

Каталог: готовое приложение · Ctrl + Enter
<div class="app">
  <div class="bar">
    <input id="q" type="search" placeholder="Поиск товара…">
    <select id="category"><option value="">Все категории</option></select>
  </div>
  <p id="status" class="status"></p>
  <ul id="list" class="grid"></ul>
  <div class="pager">
    <button id="prev">← Назад</button>
    <span id="info"></span>
    <button id="next">Вперёд →</button>
  </div>
</div>
Предпросмотр
Нажмите «Запустить», чтобы увидеть результат.
Что здесь важно заметить

Один источник правды: state. Кнопки, поиск и select не рисуют список сами, они меняют состояние и зовут load(). Один «рисовальщик»: render всегда перестраивает список целиком. Один «адресный стол»: buildUrl. Добавить новый фильтр — значит добавить поле в state, строчку в buildUrl и обработчик события. Остальное не трогаем.

Практика: достраиваем приложение

В двух задачах — то же приложение, но из него вынуто по одному куску. Допишите его: песочница сама прогонит сценарий и напечатает проверку (нужен интернет, проверка занимает несколько секунд).

ЗАДАЧА 8.1

Пагинация: go и updatePager

средняя

В приложении пусты функции updatePager (подпись «Страница N из M» и disabled для кнопок) и go(page) (проверка границ, смена страницы, загрузка). Допишите их.

Каталог: допишите пагинацию · Ctrl + Enter
<div class="app">
  <div class="bar">
    <input id="q" type="search" placeholder="Поиск товара…">
    <select id="category"><option value="">Все категории</option></select>
  </div>
  <p id="status" class="status"></p>
  <ul id="list" class="grid"></ul>
  <div class="pager">
    <button id="prev">← Назад</button>
    <span id="info"></span>
    <button id="next">Вперёд →</button>
  </div>
</div>
Предпросмотр
Нажмите «Запустить», чтобы увидеть результат.
Ожидаемый результат
карточек: 6
страниц больше одной: true
на первой странице «Назад» выкл
страница 2, карточек: 6
go(0) оставила страницу 2
назад вкл, вперёд вкл
Подсказка 1

updatePager: info.textContent = `Страница ${state.page} из ${state.pages}`; затем prev.disabled = state.page <= 1; и next.disabled = state.page >= state.pages;

Подсказка 2

go: если page < 1 || page > state.pages — просто return; иначе state.page = page; await load();

ЗАДАЧА 8.2

Поиск: search со сбросом страницы

сложная

Функция search(text) пока пустая. Она должна: убрать пробелы по краям и записать в state.q, сбросить категорию (state.category и значение catSelect), вернуть страницу на первую и загрузить данные. Тогда после поиска «zzzzqq» приложение покажет «Ничего не найдено», а пустой поиск вернёт обычный список.

Каталог: допишите поиск · Ctrl + Enter
<div class="app">
  <div class="bar">
    <input id="q" type="search" placeholder="Поиск товара…">
    <select id="category"><option value="">Все категории</option></select>
  </div>
  <p id="status" class="status"></p>
  <ul id="list" class="grid"></ul>
  <div class="pager">
    <button id="prev">← Назад</button>
    <span id="info"></span>
    <button id="next">Вперёд →</button>
  </div>
</div>
Предпросмотр
Нажмите «Запустить», чтобы увидеть результат.
Ожидаемый результат
страница: 3
поиск: карточек больше нуля: true
страница после поиска: 1
пусто: true, сообщение: Ничего не найдено
сброс поиска, карточек: 6
Подсказка 1

Четыре строки перед загрузкой: state.q = text.trim();, state.category = "";, catSelect.value = "";, state.page = 1;

Подсказка 2

Не забудьте последнюю строку — await load();. Без неё состояние поменяется, а список — нет.

09 Домашнее задание

Добавьте поиск и пагинацию в своё приложение с API. Основа — приложение из урока 14 (два API), каталог из практики сегодня или любая из тем ниже. Все API — публичные и без ключей: проверено, отвечают из браузера.

Выберите тему

ТемаAPI и документацияПоискПагинация / фильтр
A. Фильмы и сериалыTVmaze — без ключа (TMDB и OMDb требуют ключ, в курсе их не используем)https://api.tvmaze.com/search/shows?q=…: массив { score, show }; пусто — []Каталог https://api.tvmaze.com/shows?page=0 (по ~250, страницы с 0; за пределами — 404). Фильтр по жанру — на клиенте (show.genres). Постер show.image может быть null!
B. ПогодаOpen-Meteo Geocoding и ForecastПоиск города: geocoding-api.open-meteo.com/v1/search?name=…&count=10&language=ru. Если ничего не нашли, в ответе нет поля results — используйте data.results ?? []Выбранный город → api.open-meteo.com/v1/forecast?latitude=…&longitude=…&daily=temperature_2m_max,temperature_2m_min&timezone=auto. Страницы — «показать ещё дни» или листание найденных городов (count до 100)
C. КриптовалютыCoinGecko (публичный endpoint)https://api.coingecko.com/api/v3/coins/markets?vs_currency=usd&per_page=10&page=1; поиск — клиентский по name и symbolСтраницы: per_page + page. Фильтр: растёт / падает (price_change_percentage_24h). Лимит запросов небольшой: при 429 покажите понятное сообщение и не спамьте запросами
D. Рик и МортиRick and Morty API…/api/character?name=…; ничего нет — 404page, info.pages; серверные фильтры status (alive / dead / unknown) и gender. Всё на сервере — самый простой вариант

Своё публичное API без ключа тоже можно — но заранее согласуйте с преподавателем и проверьте: отвечает ли оно из браузера (CORS) и есть ли поиск и страницы.

Что должно быть в приложении

  1. Данные с API. Функция getJson (из урока 14): проверка response.ok, try / catch, понятное сообщение об ошибке.
  2. Адрес — через URLSearchParams. Ни одной склейки параметров строками. Функция buildUrl или аналог.
  3. Рендер списка. Одна функция render, которая сначала очищает; текст вставляется через textContent; есть состояние «Ничего не найдено».
  4. Поиск с debounce 300–500 мс и защитой от гонки (lastRequest или AbortController).
  5. Фильтр или сортировка — минимум одно, на сервере или на клиенте (осознанно выберите и напишите в README почему).
  6. Пагинация: страницы (кнопки «Назад» / «Вперёд» с disabled на краях и подпись «Страница N из M») или «Показать ещё». При новом поиске страница сбрасывается.
  7. Минимальные состояния: «Загрузка…», «Ничего не найдено», «Ошибка» (подробно — на следующем уроке).
  8. README: какой API взят, ссылка на документацию, как устроены параметры поиска и страниц, какой запрос вы проверяли руками.
  9. ИИ: прочитайте документацию вместе с ИИ по промпту из урока и добавьте в README: промпт, что ИИ предложил, что оказалось неверным или неточным и как вы это проверили запросом.

Что сдавать

  • Ссылку на репозиторий GitHub (папка с index.html, style.css, script.js и README).
  • Две-три фразы в README: что сделано и что оказалось самым сложным.
  • Скриншот приложения (с результатами поиска) — по желанию, как приложение к README.

Как оцениваем (0–10)

ЧтоБаллы
Данные грузятся с API, адреса через URLSearchParams, ошибки сети обработаны2
Рендер: очистка, textContent, «Ничего не найдено»2
Поиск с debounce и защитой от гонки, сброс страницы при новом поиске2
Пагинация работает на всех страницах, кнопки блокируются на краях2
Фильтр или сортировка + README и разбор ответа ИИ2
Перед сдачей

Пройдите сценарий руками: найдите что-нибудь, листните на 2-ю страницу, введите новый поиск (вернулись на 1-ю?), введите чепуху (есть «Ничего не найдено»?), отключите интернет (есть сообщение об ошибке?). Каждую строку, которую подсказал ИИ, вы должны уметь объяснить на защите.

✓ Шпаргалка урока

  • Параметры запроса идут после ?: ключ=значение через &. Собираем только через new URLSearchParams({…}) — он кодирует пробелы, & и кириллицу. params.get() всегда даёт строку или null.
  • Читаем API: документация + реальный ответ в консоли. Где список (data.products / data.results / сам массив), где total, что при «пусто» (200 и [] или 404).
  • Рендер: render(items) всегда сначала очищает (replaceChildren()), потом строит узлы; данные с сервера — через textContent; пустой результат — отдельное сообщение.
  • Поиск: input + debounce (300–500 мс) + trim() + сброс на первую страницу. От «гонки» — номер запроса lastRequest или AbortController.
  • Фильтр и сортировка: на сервере (/category/…, sortBy, order) — для больших данных; на клиенте (filter, includes) — для уже загруженных. Не все параметры API сочетаются — читайте документацию.
  • Пагинация: skip = (page − 1) × limit; страниц Math.ceil(total / limit); «Назад» выключена на 1-й, «Вперёд» — на последней; у Rick and Morty — page и info.pages / info.next.
  • Структура приложения: state → buildUrl() → load() → render() + updatePager(). События лишь меняют состояние и вызывают load().
  • С ИИ: документацию API давайте ему текстом; выдуманные параметры ловим запросом в консоли; в README записываем, что ИИ предложил и что мы исправили.