// chetaika.md | 24.06.2026

Chetaika. Книжный поисковик

Книжный поисковик поверх Google Books API: один HTML-файл, два serverless-прокси и чистый vanilla JS без зависимостей.

Задача

Сделать рабочий поисковик книг поверх Google Books API. Сложность не в запросах, а в том, что API отдаёт данные как попало: обложки по http://, рейтинги без возможности фильтрации, totalItems, который считает не всё, и молчаливый игнор maxResults больше 40.

Как делал

Два serverless-хендлера проксируют Google Books и держат ключ в переменной окружения, а не в браузере. Ответ отдаётся с s-maxage=600, stale-while-revalidate=1200 — платформа кэширует его на 10 минут и ещё 20 отдаёт устаревшую копию, пока обновляется фоновая. Для книг это бесплатный обход лимитов.

const GOOGLE_BOOKS_API = 'https://www.googleapis.com/books/v1/volumes';

module.exports = async function handler(req, res) {
  const apiKey = process.env.GOOGLE_BOOKS_API_KEY;
  const { id } = req.query;

  try {
    const response = await fetch(`${GOOGLE_BOOKS_API}/${id}?key=${apiKey}`);
    if (!response.ok) return res.status(response.status).json({ error: 'API error' });

    res.setHeader('Cache-Control', 's-maxage=600, stale-while-revalidate=1200');
    return res.status(200).json(await response.json());
  } catch (error) {
    return res.status(500).json({ error: 'Failed to fetch book details' });
  }
};

Обложки приходят по http:// и с размытым zoom=3, поэтому нормализуются цепочкой замен. Приоритет у smallThumbnail парадоксальный: с zoom=1 она отдаёт ту же картинку, что и thumbnail, но весит меньше. Если ссылки нет — рендерится SVG-плейсхолдер с градиентом.

const normalize = (url) =>
  url?.replace('http:', 'https:')?.replace('zoom=3', 'zoom=1');

const coverUrl = normalize(imageLinks?.smallThumbnail) ||
                 normalize(imageLinks?.thumbnail) || null;

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

let debounceTimer;
elements.searchInput.addEventListener('input', () => {
  clearTimeout(debounceTimer);
  debounceTimer = setTimeout(() => {
    if (elements.searchInput.value.length >= 3) handleSearch();
  }, 500);
});

Фильтр по рейтингу Google Books не поддерживает вообще, поэтому применяется на клиенте после ответа. Плата — пагинация врёт: totalItems обещает тысячи, а на экране после фильтра может остаться пять книг.

Звёзды рисуются из целой части и дробной: полная — fill="currentColor", половина — та же звезда с opacity="0.5", пустая — fill="none". Порог rating % 1 >= 0.5, потому что 4.5 должно дать половину, а 4.4 — уже нет, и простое округление вниз тут не подходит.

function createRatingStars(rating) {
  const fullStars = Math.floor(rating);
  const hasHalfStar = rating % 1 >= 0.5;
  let stars = '';

  for (let i = 0; i < 5; i++) {
    if (i < fullStars)          stars += star('currentColor');
    else if (i === fullStars && hasHalfStar) stars += star('currentColor', 0.5);
    else                          stars += star('none');
  }
  return `<div class="book-rating">${stars}</div>`;
}

Кнопка «Показать полностью» появляется только если описание действительно обрезано, а проверка идёт в requestAnimationFrame: -webkit-line-clamp применяется после рендера, и сразу после вставки в DOM scrollHeight ещё равен clientHeight.

requestAnimationFrame(() => {
  if (modalDesc.scrollHeight <= modalDesc.clientHeight) {
    toggleBtn.style.display = 'none';
    modalDesc.classList.remove('truncated');
  }
});

Итог

Три статических файла и два хендлера, ноль зависимостей: ни React, ни Tailwind, ни Axios — только fetch, CSS-переменные и localStorage. Вся кривая оболочки Google Books — разбор http:// в ссылках, фильтрация без поддержки на стороне API, кэш в заголовке и пересчёт вёрстки после смены display — закрыт на стороне фронтенда.

// похожие проекты

Ещё работы

// проект 2026
Нет изображения

Бот для сбора лидов из Telegram-каналов и RSS

Бот для Telegram: читает каналы и RSS-ленты, фильтрует заказы по ключевым словам с весами и присылает подходящие в личку.

// проект 2026
Нет изображения

Парсер публикаций ВКонтакте

Парсер для VK: находит подходящие посты в поиске новостей и ставит их в предложку своих групп, с защитой от повторов.

// проект 2026
Скриншот главной страницы лединга AROMA

Aroma. Лендинг кофейни

Одностраничный лендинг кофейни: атмосфера, меню с фильтрами и каруселью, форма бронирования с модалкой подтверждения.

// проект 2026
Скриншот главной страницы лендинга DRIVE24

DRIVE24. Лендинг сервиса каршеринга

Лендинг сервиса каршеринга на WordPress + Elementor: тарифы, автопарк, инструкция, приложение, отзывы и форма заявки.

// проект 2026
Нет изображения

Парсер цены товара Ozon по ID

Скрипт по ID товара Ozon читает цену покупателя из зелёной плашки, отдаёт её в JSON и сам переживает антибот-проверку Ozon.

// проект 2026
Нет изображения

Telegram парсер участников каналов

Консольный инструмент на Telethon: инвентаризация участников Telegram-групп с чекпоинтами и обходом FloodWait.

> все проекты