# Посібник з JavaScript API фільмів

> Напишіть скрипт Node.js, який приймає список id фільмів і друкує картку для кожного: назву, рік, режисера, головних акторів і всі відомі рейтинги. Він використовує вбудований fetch, тому пакети npm не потрібні. Акторський склад і рейтинги надходять в одному запиті через append_to_response.

Ви напишете `cards.mjs`, скрипт Node.js. Передайте йому id фільмів, і він надрукує по одній картці на фільм із назвою, роком, режисером, головними акторами та всіма відомими рейтингами. Він використовує вбудований `fetch`, потребує Node 20 або новішої та займає близько 15 хвилин.

## Крок 1: отримайте ключ API

Тарифи працюють за передплатою й відрізняються лише швидкістю: 20, 40 або 120 запитів на хвилину, з усіма даними на кожному тарифі. Оберіть тариф на сторінці [ціни](https://amodb.dev/uk/pricing/), створіть ключ у панелі облікового запису, потім покладіть ключ у середовище:

```bash
export AMODB_KEY="YOUR_API_KEY"
```

Скрипт читає `process.env.AMODB_KEY`, тож ключ ніколи не потрапляє ні у вихідний код, ні в систему контролю версій.

## Крок 2: створіть проєкт

Нічого встановлювати не треба. Створіть теку й порожній `cards.mjs`:

```bash
mkdir movie-cards && cd movie-cards
node --version
```

Версія має вивести `v20` або вище. Розширення `.mjs` вмикає модулі ES без `package.json`.

## Крок 3: надішліть перший запит

Викличте `GET /3/movie/{movie_id}` з ключем у заголовку `Authorization` і тайм-аутом, потім перевірте статус, перш ніж розбирати тіло.

```js
const res = await fetch('https://api.amodb.dev/3/movie/550?language=en-US', {
  headers: { Authorization: `Bearer ${process.env.AMODB_KEY}` },
  signal: AbortSignal.timeout(10_000),
});
console.log(res.status, (await res.json()).title);
```

## Крок 4: додайте акторський склад і рейтинги

Додавання `credits,ratings` до того самого виклику повертає список акторів, список знімальної групи та п'ять джерел рейтингів під власними ключами. Це один запит на фільм замість трьох, і весь виклик зараховується один раз до ваших 20, 40 або 120 запитів на хвилину. Режисер це учасник знімальної групи, чий `job` дорівнює `Director`. Поля описано на сторінках [акторський склад і знімальна група](https://amodb.dev/uk/credits/) та [рейтинги](https://amodb.dev/uk/ratings/).

У `ratings` кожне джерело це `{ value, votes }` або `null`. Форматувальник картки нижче пропускає значення null і з'єднує решту:

```js
const scores = Object.entries(movie.ratings ?? {})
  .filter(([, entry]) => entry !== null)
  .map(([source, entry]) => `${source} ${entry.value}`)
  .join(', ');
```

## Крок 5: обробка помилок і лімітів запитів

Кожен збій відповідає певному рішенню:

| Статус                    | Рішення                                                                                                  |
| ------------------------- | -------------------------------------------------------------------------------------------------------- |
| 401                       | Зупиніть увесь запуск. Хибний або призупинений ключ ламає кожен id.                                      |
| 404                       | Повідомте про id і переходьте до наступного.                                                             |
| 429                       | Зачекайте стільки секунд, скільки вказано в `Retry-After` (не більше 30), і повторіть, до двох повторів. |
| 504                       | Повторюйте за тим самим розкладом, потім повідомте.                                                      |
| Тайм-аут або немає мережі | Зачекайте одну секунду, помножену на номер спроби, після третього збою повідомте.                        |

Заголовок `X-RateLimit-Remaining` показує, скільки викликів лишилося в поточному 60-секундному вікні. Логуйте його, якщо хочете сповільнитися до появи 429. Правила вікна описано на сторінці [ліміти запитів](https://amodb.dev/uk/rate-limits/), а повний список кодів на сторінці [помилки](https://amodb.dev/uk/errors/).

## Крок 6: додайте невеликий кеш

Деталі кешуються на периферії на 6 годин, тож повторний запит протягом цього часу повертає ті самі дані. Кешуйте й у себе, щоб узагалі пропустити виклик. Скрипт тримає `Map` із записами `{ expires, data }`, завантажує його з `.amodb-cache.json` на старті й записує назад наприкінці. Другий запуск для тих самих id не робить жодного запиту.

## Крок 7: повна програма

Збережіть це як `cards.mjs` і запустіть `node cards.mjs 550 603 27205`.

```js
import { readFile, writeFile } from 'node:fs/promises';

const BASE = 'https://api.amodb.dev/3';
const KEY = process.env.AMODB_KEY;
const CACHE_FILE = '.amodb-cache.json';
const DETAILS_TTL_MS = 6 * 60 * 60 * 1000;

if (!KEY) {
  console.error('Set AMODB_KEY first.');
  process.exit(1);
}

class ApiError extends Error {
  constructor(status, message) {
    super(`${status}: ${message}`);
    this.status = status;
  }
}

const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));

async function request(path, params = {}) {
  const url = new URL(BASE + path);
  for (const [name, value] of Object.entries(params)) url.searchParams.set(name, value);

  for (let attempt = 1; attempt <= 3; attempt++) {
    let res;
    try {
      res = await fetch(url, {
        headers: { Authorization: `Bearer ${KEY}` },
        signal: AbortSignal.timeout(10_000),
      });
    } catch (err) {
      if (attempt === 3) throw new Error(`network error: ${err.message}`);
      await sleep(1000 * attempt);
      continue;
    }
    if (res.status === 401) {
      throw new ApiError(401, 'the key is invalid or suspended. Check AMODB_KEY.');
    }
    if ((res.status === 429 || res.status === 504) && attempt < 3) {
      const seconds = Math.min(Number(res.headers.get('Retry-After')) || 1, 30);
      await sleep(seconds * 1000);
      continue;
    }
    if (!res.ok) {
      const body = await res.json().catch(() => ({}));
      throw new ApiError(res.status, body.status_message ?? res.statusText);
    }
    return res.json();
  }
}

async function loadCache() {
  try {
    return new Map(Object.entries(JSON.parse(await readFile(CACHE_FILE, 'utf8'))));
  } catch {
    return new Map();
  }
}

async function cachedGet(cache, path, params, ttlMs) {
  const key = `${path}?${new URLSearchParams(params)}`;
  const hit = cache.get(key);
  if (hit && hit.expires > Date.now()) return hit.data;
  const data = await request(path, params);
  cache.set(key, { expires: Date.now() + ttlMs, data });
  return data;
}

function formatCard(movie) {
  const year = (movie.release_date ?? '').slice(0, 4) || 'n/a';
  const director = movie.credits.crew.find((p) => p.job === 'Director')?.name ?? 'unknown';
  const cast = movie.credits.cast.slice(0, 3).map((p) => p.name).join(', ');
  const scores = Object.entries(movie.ratings ?? {})
    .filter(([, entry]) => entry !== null)
    .map(([source, entry]) => `${source} ${entry.value}`)
    .join(', ');
  return [
    `${movie.title} (${year}), ${movie.runtime ?? '?'} min, directed by ${director}`,
    `  Cast: ${cast || 'none listed'}`,
    `  Ratings: ${scores || 'none known'}`,
  ].join('\n');
}

const ids = process.argv.slice(2);
if (ids.length === 0) {
  console.error('Usage: node cards.mjs 550 603 27205');
  process.exit(1);
}

const cache = await loadCache();
let failed = false;
for (const id of ids) {
  try {
    const params = { language: 'en-US', append_to_response: 'credits,ratings' };
    console.log(formatCard(await cachedGet(cache, `/movie/${id}`, params, DETAILS_TTL_MS)));
  } catch (err) {
    console.error(`id ${id}: ${err.message}`);
    failed = true;
    if (err.status === 401) break;
  }
}
await writeFile(CACHE_FILE, JSON.stringify(Object.fromEntries(cache)));
process.exitCode = failed ? 1 : 0;
```

Запустіть двічі. Перший запуск надсилає по одному запиту на id, плюс повтор, якщо повернеться 429, і записує `.amodb-cache.json`. Другий запуск друкує ті самі картки й нічого не надсилає. Видаліть файл, щоб примусово оновити дані. Id, якого не існує, друкує `404: The resource you requested could not be found.` і встановлює код виходу 1, що корисно, коли скрипт працює всередині запланованого завдання.

Цикл послідовний навмисно. Запуск усіх id одразу вичерпав би хвилинний бюджет одним сплеском і для більшості спричинив би шлях 429. Якщо потрібна швидкість, обробляйте id групами по п'ять і залиште логіку повторів у `request()`.

## Наступні кроки

- Перетворіть цикл на невелику функцію, що повертає картки, і викличте її з обробника HTTP на власному сервері. Тримайте ключ на сервері.
- Замініть список id результатами зі [списків трендів](https://amodb.dev/uk/trending-lists/) і надрукуйте картку для кожного.
- Додайте `moods` або `soundtracks` до `append_to_response`. Настрої надходять як `moods.results[]` з `name`, `avg` і `values` за шкалою від 0 до 10, а `moods.source` має значення `measured` або `estimated`. Саундтреки надходять як `soundtracks.results[]`, порожній, коли альбом невідомий. Дивіться [настрої](https://amodb.dev/uk/moods/) і [саундтреки](https://amodb.dev/uk/soundtracks/).
- Порівняйте те саме завдання на [Python](https://amodb.dev/uk/python-movie-api/) або прочитайте [швидкий старт](https://amodb.dev/uk/quickstart/).

## Запитання й відповіді

### Чи потрібні цьому скрипту пакети npm?

Ні. Node 20 або новіша має fetch, URL і AbortSignal.timeout, тож у скрипта немає залежностей. Розширення .mjs дозволяє використовувати оператори import і await верхнього рівня. Запускайте командою node cards.mjs та список id.

### Чи можна викликати amodb просто з браузерного JavaScript?

Уникайте цього. Ключ у фронтенд-коді бачить кожен відвідувач, його можна скопіювати й вичерпати. Викликайте amodb із Node або власного сервера й віддавайте сторінці лише потрібні поля. Зображення не потребують ключа й завантажуються напряму.

### Скільки id можна обробити за хвилину?

Один запит на кожен id, якого немає в кеші, бо акторський склад і рейтинги надходять через append_to_response. Тариф на 20 запитів на хвилину покриває близько 20 нових id за хвилину, а тарифи на 40 і 120 масштабують це. Повтор при 429 поглинає короткі сплески.

### Чому на картці бракує деяких рейтингів?

Кожне з п'яти джерел рейтингів є об'єктом із value і votes або null, якщо джерело не знає цей фільм. Скрипт відкидає null, тому один фільм може показати чотири оцінки, а інший жодної.

### Де тримати кеш у довготривалому сервісі?

Тримайте Map у пам'яті й обійдіться без файлу. Для деталей задайте TTL 6 годин, як у кеші на периферії, і видаляйте застарілі записи під час читання, щоб Map не розростався безмежно.

## Пов'язані сторінки

- [API деталей фільму](https://amodb.dev/uk/movies/): API деталей фільму з тими самими шляхами та JSON, що в TMDB v3. Отримайте фільм за id, додайте акторів, рейтинги й настрої через append_to_response, 14 мовами.
- [API акторського складу та знімальної групи](https://amodb.dev/uk/credits/): API credits зі шляхами та JSON, як у TMDB v3. Отримайте акторський склад і знімальну групу фільмів, серіалів, сезонів і серій з полями order, department і job.
- [Рейтинги фільмів і серіалів](https://amodb.dev/uk/ratings/): API рейтингів фільмів повертає оцінки IMDb, Kinopoisk, Metacritic, Rotten Tomatoes і TMDB для фільму чи серіалу одним викликом, з null для невідомих джерел.
- [Автентифікація](https://amodb.dev/uk/authentication/): Як надсилати ключ amodb: api_key у запиті, заголовок Authorization Bearer або apikey для викликів OMDb. Формат ключа, зберігання, заміна та помилки 401.
- [Ліміти запитів](https://amodb.dev/uk/rate-limits/): amodb обмежує кожен ключ запитами на хвилину в ковзному вікні 60 секунд: 20, 40 або 120 за тарифом. Читайте заголовки ліміту й обробляйте 429 із Retry-After.
- [Посібник з Python API фільмів](https://amodb.dev/uk/python-movie-api/): Створіть консольний інструмент Python, що шукає фільм за назвою й друкує рік, акторів, рейтинг IMDb і постер, з повтором при 429 і файловим кешем. На requests.

Оновлено: 2026-10-18. Source: https://amodb.dev/uk/javascript-movie-api/
