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.
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.
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.
mkdir movie-cards && cd movie-cards
node --versionPaso 3: envía la primera solicitud
Llama a GET / con la clave en un encabezado Authorization y un tiempo de espera, y comprueba el estado antes de analizar el cuerpo.
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:
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:
| Estado | Decisión |
|---|---|
| 401 | Detén toda la ejecución. Una clave incorrecta o suspendida falla con todos los ids. |
| 404 | Informa del id y continúa con el siguiente. |
| 429 | Espera Retry-After segundos (tope de 30) y reintenta, hasta dos reintentos. |
| 504 | Reintenta con el mismo calendario y luego informa. |
| Tiempo agotado o sin red | Espera 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().
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
moodsosoundtracksaappend_to_response. Los moods (ambiente emocional) llegan comomoods.results[]conname,avgyvaluesen una escala de 0 a 10, ymoods.sourceesmeasuredoestimated. Las bandas sonoras llegan comosoundtracks.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.