# 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](https://amodb.dev/es/pricing/), crea una clave en el panel de tu cuenta y luego pon la clave en el entorno:

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

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

## Paso 2: crea el proyecto

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

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

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

## 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.

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

## 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](https://amodb.dev/es/credits/) y [puntuaciones](https://amodb.dev/es/ratings/).

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

```js
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](https://amodb.dev/es/rate-limits/) para ver las reglas de la ventana y [errores](https://amodb.dev/es/errors/) 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`.

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

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()`.

## 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](https://amodb.dev/es/trending-lists/) 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](https://amodb.dev/es/moods/) y [bandas sonoras](https://amodb.dev/es/soundtracks/).
- Compara la misma tarea en [Python](https://amodb.dev/es/python-movie-api/) o lee el [inicio rápido](https://amodb.dev/es/quickstart/).

## Preguntas y respuestas

### ¿Este script necesita algún paquete de npm?

No. Node 20 o superior incluye fetch, URL y AbortSignal.timeout, así que el script no tiene dependencias. La extensión .mjs permite usar sentencias import y await en el nivel superior. Ejecútalo con node cards.mjs seguido de los ids.

### ¿Puedo llamar a amodb directamente desde JavaScript en el navegador?

Evítalo. Una clave en el código del front end es visible para todos los visitantes y puede copiarse y agotarse. Llama a amodb desde Node o desde tu propio servidor y envía a la página solo los campos que necesita. Las imágenes no necesitan clave y se cargan directamente.

### ¿Cuántos ids puedo procesar por minuto?

Una solicitud por cada id sin caché, porque los créditos y las puntuaciones llegan mediante append_to_response. El plan de 20 solicitudes por minuto cubre unos 20 ids nuevos por minuto, y los planes de 40 y 120 escalan a partir de ahí. El reintento ante 429 absorbe ráfagas cortas.

### ¿Por qué faltan algunas puntuaciones en una ficha?

Cada una de las cinco fuentes de puntuación es un objeto con value y votes, o null cuando esa fuente no se conoce para el título. El script descarta los valores nulos, así que un título puede mostrar cuatro notas y otro ninguna.

### ¿Dónde debe vivir el caché en un servicio de larga ejecución?

Mantén el Map en memoria y omite el archivo. Usa un TTL de 6 horas para los detalles, igual que el caché perimetral, y elimina las entradas caducadas al leerlas para que el Map no crezca sin límite.

## Páginas relacionadas

- [API de detalles de película](https://amodb.dev/es/movies/): API de detalles de película con rutas y JSON de TMDB v3. Obtén una película por id y añade créditos, puntuaciones y moods con append_to_response, en 14 idiomas.
- [API de créditos](https://amodb.dev/es/credits/): API de créditos con las mismas rutas y el mismo JSON que TMDB v3. Obtén reparto y equipo técnico de películas, series, temporadas y episodios con order y job.
- [Puntuaciones de películas y series](https://amodb.dev/es/ratings/): API de puntuaciones de películas: IMDb, Kinopoisk, Metacritic, Rotten Tomatoes y TMDB de un título en una sola llamada, con null si falta una fuente.
- [Autenticación](https://amodb.dev/es/authentication/): Cómo enviar tu clave de amodb: api_key en la consulta, un encabezado Bearer o apikey en llamadas OMDb. Formato, almacenamiento, rotación y errores 401.
- [Límites de solicitudes](https://amodb.dev/es/rate-limits/): amodb limita cada clave por solicitudes por minuto en una ventana deslizante de 60 segundos: 20, 40 o 120 según el plan. Lee los encabezados y gestiona el 429.
- [Tutorial de API de películas en Python](https://amodb.dev/es/python-movie-api/): Crea una herramienta de línea de comandos en Python que busca una película y muestra año, reparto, nota de IMDb y póster, con reintento ante 429 y caché.

Actualizado: 2026-10-18. Source: https://amodb.dev/es/javascript-movie-api/
