JavaScript movie API tutorial

Write a Node.js script that takes a list of movie ids and prints a card for each: title, year, director, top cast and every known rating. It uses the built-in fetch, so there are no npm packages. Credits and ratings arrive in the same request through append_to_response.

.js JavaScript Updated View as Markdown

You will write cards.mjs, a Node.js script. Give it movie ids and it prints one card per movie with title, year, director, top cast and every known rating. It uses the built-in fetch, needs Node 20 or newer and takes about 15 minutes.

Step 1: Get an API key

Plans are prepaid and differ only in speed: 20, 40 or 120 requests per minute, with all data on every plan. Choose one on pricing, create a key in your account dashboard, then put the key in the environment:

The script reads process.env.AMODB_KEY, so the key never lands in the source or in version control.

Shell
export AMODB_KEY="YOUR_API_KEY"

Step 2: Create the project

There is nothing to install. Make a folder and an empty cards.mjs:

The version must print v20 or higher. The .mjs extension turns on ES modules without a package.json.

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

Step 3: Send the first request

Call GET /3/movie/{movie_id} with the key in an Authorization header and a timeout, then check the status before you parse the body.

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

Step 4: Add credits and ratings

Appending credits,ratings to the same call returns the cast list, the crew list and the five rating sources under their own keys. That is one request per movie instead of three, and the whole call counts once against your 20, 40 or 120 requests per minute. The director is the crew member whose job is Director. Fields are described on credits and ratings.

In ratings, each source is { value, votes } or null. The card formatter below skips nulls and joins the rest:

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

Step 5: Handle errors and rate limits

Every failure maps to a decision:

StatusDecision
401Stop the whole run. A wrong or suspended key fails every id.
404Report the id and continue with the next one.
429Wait Retry-After seconds (cap 30) and retry, up to two retries.
504Retry on the same schedule, then report.
Timeout or no networkWait one second times the attempt number, then report on the third failure.

The X-RateLimit-Remaining header shows how many calls are left in the current 60 second window. Log it if you want to slow down before a 429 happens. See rate limits for the window rules and errors for the full list.

Step 6: Add a small cache

Details are cached at the edge for 6 hours, so asking again inside that time returns the same data. Cache on your side to skip the call altogether. The script keeps a Map of { expires, data } entries, loads it from .amodb-cache.json on start and writes it back at the end. A second run over the same ids makes no requests.

Step 7: The complete program

Save this as cards.mjs and run node cards.mjs 550 603 27205.

Run it twice. The first run sends one request per id, plus a retry if a 429 comes back, and writes .amodb-cache.json. The second run prints the same cards and sends nothing. Delete the file to force a refresh. An id that does not exist prints 404: The resource you requested could not be found. and sets the exit code to 1, which helps when the script runs inside a scheduled job.

The loop is sequential on purpose. Running all ids at once would burn through a per-minute budget in one burst and trigger the 429 path for most of them. If you need speed, process ids in groups of five and keep the retry logic in 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;

Next steps

  • Turn the loop into a small function that returns cards, then call it from an HTTP handler on your own server. Keep the key on the server.
  • Swap the id list for trending results and print a card for each.
  • Add moods or soundtracks to append_to_response. Moods arrive as moods.results[] with name, avg and values on a 0 to 10 scale, and moods.source is measured or estimated. Soundtracks arrive as soundtracks.results[], empty when no album is known. See moods and soundtracks.
  • Compare the same task in Python or read the quickstart.