Tutorial de API de películas en JavaScript

Escribe un script de Node.js que recibe una lista de ids de películas e imprime una ficha de cada una: título, año, director, reparto principal y todas las puntuaciones conocidas. Usa el fetch integrado, así que no hay paquetes de npm. Los créditos y las puntuaciones llegan en la misma solicitud mediante append_to_response.

.js JavaScript Actualizado el Ver en Markdown

Vas a escribir cards.mjs, un script de Node.js. Pásale ids de películas e imprime una ficha por película con título, año, director, reparto principal y todas las puntuaciones conocidas. Usa el fetch integrado, requiere Node 20 o superior y lleva unos 15 minutos.

Paso 1: obtén una clave de API

Los planes son prepago y se diferencian solo en la velocidad: 20, 40 o 120 solicitudes por minuto, con todos los datos en cada plan. Elige uno en precios, crea una clave en el panel de tu cuenta y luego pon la clave en el entorno:

El script lee process.env.AMODB_KEY, así que la clave nunca acaba en el código fuente ni en el control de versiones.

Shell
export AMODB_KEY="YOUR_API_KEY"

Paso 2: crea el proyecto

No hay nada que instalar. Crea una carpeta y un cards.mjs vacío:

La versión debe mostrar v20 o superior. La extensión .mjs activa los módulos ES sin necesidad de package.json.

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

Paso 3: envía la primera solicitud

Llama a GET /3/movie/{movie_id} con la clave en un encabezado Authorization y un tiempo de espera, y comprueba el estado antes de analizar el cuerpo.

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

Paso 4: añade créditos y puntuaciones

Si añades credits,ratings a la misma llamada, obtienes la lista de reparto, la lista del equipo técnico y las cinco fuentes de puntuación bajo sus propias claves. Es una solicitud por película en lugar de tres, y toda la llamada cuenta una sola vez para tus 20, 40 o 120 solicitudes por minuto. El director es el miembro del equipo técnico cuyo job es Director. Los campos se describen en créditos y puntuaciones.

En ratings, cada fuente es { value, votes } o null. El formateador de fichas de abajo omite los valores nulos y une el resto:

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

Paso 5: gestiona errores y límites de solicitudes

Cada fallo se traduce en una decisión:

EstadoDecisión
401Detén toda la ejecución. Una clave incorrecta o suspendida falla con todos los ids.
404Informa del id y continúa con el siguiente.
429Espera Retry-After segundos (tope de 30) y reintenta, hasta dos reintentos.
504Reintenta con el mismo calendario y luego informa.
Tiempo agotado o sin redEspera un segundo multiplicado por el número de intento y informa en el tercer fallo.

El encabezado X-RateLimit-Remaining muestra cuántas llamadas quedan en la ventana actual de 60 segundos. Regístralo si quieres reducir el ritmo antes de que llegue un 429. Consulta límites de solicitudes para ver las reglas de la ventana y errores para la lista completa.

Paso 6: añade un caché pequeño

Los detalles se guardan en el caché perimetral durante 6 horas, así que volver a pedirlos en ese tiempo devuelve los mismos datos. Guarda un caché por tu parte para omitir la llamada por completo. El script mantiene un Map de entradas { expires, data }, lo carga desde .amodb-cache.json al iniciar y lo vuelve a escribir al final. Una segunda ejecución sobre los mismos ids no hace ninguna solicitud.

Paso 7: el programa completo

Guárdalo como cards.mjs y ejecuta node cards.mjs 550 603 27205.

Ejecútalo dos veces. La primera ejecución envía una solicitud por id, más un reintento si vuelve un 429, y escribe .amodb-cache.json. La segunda imprime las mismas fichas y no envía nada. Borra el archivo para forzar una actualización. Un id que no existe imprime 404: The resource you requested could not be found. y deja el código de salida en 1, lo cual ayuda cuando el script corre dentro de una tarea programada.

El bucle es secuencial a propósito. Ejecutar todos los ids a la vez agotaría un presupuesto por minuto en una sola ráfaga y activaría la ruta del 429 para la mayoría. Si necesitas velocidad, procesa los ids en grupos de cinco y mantén la lógica de reintento en 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;

Próximos pasos

  • Convierte el bucle en una función pequeña que devuelva fichas y llámala desde un manejador HTTP en tu propio servidor. Mantén la clave en el servidor.
  • Sustituye la lista de ids por resultados de tendencias e imprime una ficha para cada uno.
  • Añade moods o soundtracks a append_to_response. Los moods (ambiente emocional) llegan como moods.results[] con name, avg y values en una escala de 0 a 10, y moods.source es measured o estimated. Las bandas sonoras llegan como soundtracks.results[], vacío cuando no se conoce ningún álbum. Consulta moods y bandas sonoras.
  • Compara la misma tarea en Python o lee el inicio rápido.