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, 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.
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.
mkdir movie-cards && cd movie-cards
node --versionStep 3: Send the first request
Call GET / with the key in an Authorization header and a timeout, then check the status before you parse the body.
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:
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 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().
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
moodsorsoundtrackstoappend_to_response. Moods arrive asmoods.results[]withname,avgandvalueson a 0 to 10 scale, andmoods.sourceismeasuredorestimated. Soundtracks arrive assoundtracks.results[], empty when no album is known. See moods and soundtracks. - Compare the same task in Python or read the quickstart.