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

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](https://amodb.dev/pricing/), create a key in your account dashboard, then put the key in the environment:

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

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

## Step 2: Create the project

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

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

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

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

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

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

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

```js
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:

| Status                | Decision                                                                    |
| --------------------- | --------------------------------------------------------------------------- |
| 401                   | Stop the whole run. A wrong or suspended key fails every id.                |
| 404                   | Report the id and continue with the next one.                               |
| 429                   | Wait `Retry-After` seconds (cap 30) and retry, up to two retries.           |
| 504                   | Retry on the same schedule, then report.                                    |
| Timeout or no network | Wait 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](https://amodb.dev/rate-limits/) for the window rules and [errors](https://amodb.dev/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`.

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

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

## 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](https://amodb.dev/trending-lists/) 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](https://amodb.dev/moods/) and [soundtracks](https://amodb.dev/soundtracks/).
- Compare the same task in [Python](https://amodb.dev/python-movie-api/) or read the [quickstart](https://amodb.dev/quickstart/).

## Questions and answers

### Does this script need any npm packages?

No. Node 20 or newer ships fetch, URL and AbortSignal.timeout, so the script has zero dependencies. The .mjs extension lets you use import statements and top level await. Run it with node cards.mjs followed by ids.

### Can I call amodb straight from browser JavaScript?

Avoid it. A key in front end code is visible to every visitor and can be copied and used up. Call amodb from Node or your own server and send the page only the fields it needs. Images need no key and load directly.

### How many ids can I process in a minute?

One request per uncached id, because credits and ratings arrive through append_to_response. The 20 requests per minute plan covers about 20 fresh ids a minute, and the 40 and 120 plans scale up. The 429 retry absorbs short bursts.

### Why are some ratings missing from a card?

Each of the five rating sources is an object with value and votes, or null when that source is not known for the title. The script drops nulls, so one title can show four scores and another can show none.

### Where should the cache live in a long running service?

Keep the Map in memory and skip the file. Use a 6 hour TTL for details to match the edge cache, and delete expired entries on read so the Map does not grow without limit.

## Related pages

- [Movie details API](https://amodb.dev/movies/): Movie details API with the same paths and JSON as TMDB v3. Get one movie by id, add credits, ratings and moods with append_to_response, in 14 languages.
- [Credits API](https://amodb.dev/credits/): Credits API with the same paths and JSON as TMDB v3. Get cast and crew for movies, series, seasons and episodes, with order, department and job fields.
- [Movie and TV ratings](https://amodb.dev/ratings/): Movie ratings API that returns IMDb, Kinopoisk, Metacritic, Rotten Tomatoes and TMDB scores for a title in one call, with null for any source not known.
- [Authentication](https://amodb.dev/authentication/): How to send your amodb key: api_key in the query, an Authorization Bearer header, or apikey for OMDb calls. Key format, storage, rotation and the 401 errors.
- [Rate limits](https://amodb.dev/rate-limits/): amodb limits each key by requests per minute in a sliding 60 second window: 20, 40 or 120 by plan. Read the rate limit headers and handle 429 with Retry-After.
- [Python movie API tutorial](https://amodb.dev/python-movie-api/): Build a Python command line tool that searches a movie title and prints year, cast, IMDb rating and poster, with 429 retry and a file cache. Uses requests.

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