Tutoriel API de films JavaScript

Écrivez un script Node.js qui prend une liste d'identifiants de films et affiche une fiche pour chacun : titre, année, réalisateur, distribution principale et toutes les notes connues. Il utilise le fetch intégré, donc aucun paquet npm. Crédits et notes arrivent dans la même requête grâce à append_to_response.

.js JavaScript Mis à jour le Voir en Markdown

Vous allez écrire cards.mjs, un script Node.js. Donnez-lui des identifiants de films et il affiche une fiche par film avec le titre, l'année, le réalisateur, la distribution principale et toutes les notes connues. Il utilise le fetch intégré, demande Node 20 ou plus récent et prend environ 15 minutes.

Étape 1 : obtenir une clé d'API

Les offres sont prépayées et ne diffèrent que par la vitesse : 20, 40 ou 120 requêtes par minute, avec toutes les données dans chaque offre. Choisissez-en une sur les tarifs, créez une clé dans le tableau de bord de votre compte, puis placez la clé dans l'environnement :

Le script lit process.env.AMODB_KEY : la clé ne se retrouve donc jamais dans le code source ni dans le contrôle de version.

Shell
export AMODB_KEY="YOUR_API_KEY"

Étape 2 : créer le projet

Il n'y a rien à installer. Créez un dossier et un fichier cards.mjs vide :

La version doit afficher v20 ou plus. L'extension .mjs active les modules ES sans package.json.

Shell
mkdir movie-cards && cd movie-cards
node --version

Étape 3 : envoyer la première requête

Appelez GET /3/movie/{movie_id} avec la clé dans un en-tête Authorization et un délai d'expiration, puis vérifiez le statut avant d'analyser le corps.

JavaScript
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);

Étape 4 : ajouter les crédits et les notes

Ajouter credits,ratings au même appel renvoie la liste de la distribution, la liste de l'équipe et les cinq sources de notes sous leurs propres clés. Cela fait une requête par film au lieu de trois, et l'appel entier compte une fois dans vos 20, 40 ou 120 requêtes par minute. Le réalisateur est le membre de l'équipe dont le job est Director. Les champs sont décrits sur crédits et notes.

Dans ratings, chaque source est { value, votes } ou null. Le formateur de fiche ci-dessous ignore les null et joint le reste :

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

Étape 5 : gérer les erreurs et les limites de débit

Chaque échec correspond à une décision :

StatutDécision
401Arrêter toute l'exécution. Une clé fausse ou suspendue fait échouer chaque identifiant.
404Signaler l'identifiant et continuer avec le suivant.
429Attendre Retry-After secondes (plafond 30) et réessayer, jusqu'à deux nouvelles tentatives.
504Réessayer selon le même calendrier, puis signaler.
Délai dépassé ou pas de réseauAttendre une seconde multipliée par le numéro de la tentative, puis signaler au troisième échec.

L'en-tête X-RateLimit-Remaining indique combien d'appels restent dans la fenêtre de 60 secondes en cours. Consignez-le si vous voulez ralentir avant qu'un 429 ne se produise. Voir limites de débit pour les règles de la fenêtre et erreurs pour la liste complète.

Étape 6 : ajouter un petit cache

Les détails sont mis en cache 6 heures en périphérie : redemander dans ce délai renvoie les mêmes données. Mettez en cache de votre côté pour éviter complètement l'appel. Le script garde une Map d'entrées { expires, data }, la charge depuis .amodb-cache.json au démarrage et la réécrit à la fin. Une seconde exécution sur les mêmes identifiants ne fait aucune requête.

Étape 7 : le programme complet

Enregistrez ceci sous cards.mjs et lancez node cards.mjs 550 603 27205.

Lancez-le deux fois. La première exécution envoie une requête par identifiant, plus une nouvelle tentative si un 429 revient, et écrit .amodb-cache.json. La seconde affiche les mêmes fiches et n'envoie rien. Supprimez le fichier pour forcer une actualisation. Un identifiant qui n'existe pas affiche 404: The resource you requested could not be found. et met le code de sortie à 1, ce qui aide quand le script tourne dans une tâche planifiée.

La boucle est séquentielle volontairement. Lancer tous les identifiants à la fois consommerait un budget par minute en une seule rafale et déclencherait le chemin 429 pour la plupart d'entre eux. Si vous avez besoin de vitesse, traitez les identifiants par groupes de cinq et gardez la logique de nouvelle tentative dans request().

JavaScript
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;

Pour aller plus loin

  • Transformez la boucle en petite fonction qui renvoie des fiches, puis appelez-la depuis un gestionnaire HTTP de votre propre serveur. Gardez la clé côté serveur.
  • Remplacez la liste d'identifiants par les résultats de tendances et affichez une fiche pour chacun.
  • Ajoutez moods ou soundtracks à append_to_response. Les ambiances arrivent sous la forme moods.results[] avec name, avg et values sur une échelle de 0 à 10, et moods.source vaut measured ou estimated. Les bandes originales arrivent sous la forme soundtracks.results[], vide quand aucun album n'est connu. Voir ambiances et bandes originales.
  • Comparez la même tâche en Python ou lisez le démarrage rapide.