Tutorial de API de filmes em JavaScript
Escreva um script Node.js que recebe uma lista de ids de filmes e imprime um cartão para cada um: título, ano, diretor, elenco principal e todas as avaliações conhecidas. Usa o fetch embutido, então não há pacotes npm. Créditos e avaliações chegam na mesma requisição por meio de append_to_response.
Você vai escrever cards.mjs, um script Node.js. Passe ids de filmes e ele imprime um cartão por filme com título, ano, diretor, elenco principal e todas as avaliações conhecidas. Usa o fetch embutido, exige Node 20 ou mais recente e leva cerca de 15 minutos.
Passo 1: obtenha uma chave de API
Os planos são pré-pagos e diferem só na velocidade: 20, 40 ou 120 requisições por minuto, com todos os dados em todos os planos. Escolha um em preços, crie uma chave no painel da sua conta e coloque-a no ambiente:
O script lê process.env.AMODB_KEY, então a chave nunca vai parar no código-fonte nem no controle de versão.
export AMODB_KEY="YOUR_API_KEY"Passo 2: crie o projeto
Não há nada para instalar. Crie uma pasta e um cards.mjs vazio:
A versão deve mostrar v20 ou superior. A extensão .mjs ativa módulos ES sem um package.json.
mkdir movie-cards && cd movie-cards
node --versionPasso 3: envie a primeira requisição
Chame GET / com a chave em um cabeçalho Authorization e um timeout, e confira o status antes de analisar o corpo.
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);Passo 4: adicione créditos e avaliações
Anexar credits,ratings à mesma chamada devolve a lista de elenco, a lista da equipe técnica e as cinco fontes de avaliação, cada uma na sua própria chave. É uma requisição por filme em vez de três, e a chamada inteira conta uma vez nas suas 20, 40 ou 120 requisições por minuto. O diretor é o membro da equipe cujo job é Director. Os campos estão descritos em créditos e avaliações.
Em ratings, cada fonte é { value, votes } ou null. O formatador de cartão abaixo ignora os nulos e junta o resto:
const scores = Object.entries(movie.ratings ?? {})
.filter(([, entry]) => entry !== null)
.map(([source, entry]) => `${source} ${entry.value}`)
.join(', ');Passo 5: trate erros e limites de requisições
Toda falha leva a uma decisão:
| Status | Decisão |
|---|---|
| 401 | Interrompa a execução inteira. Uma chave errada ou suspensa falha em todo id. |
| 404 | Informe o id e continue com o próximo. |
| 429 | Espere Retry-After segundos (máximo 30) e tente de novo, com até duas repetições. |
| 504 | Tente de novo no mesmo esquema e depois informe. |
| Timeout ou sem rede | Espere um segundo vezes o número da tentativa e informe na terceira falha. |
O cabeçalho X-RateLimit-Remaining mostra quantas chamadas restam na janela atual de 60 segundos. Registre-o no log se quiser desacelerar antes de um 429. Veja limites de requisições para as regras da janela e erros para a lista completa.
Passo 6: adicione um cache pequeno
Os detalhes ficam em cache na borda por 6 horas, então perguntar de novo nesse intervalo devolve os mesmos dados. Faça cache do seu lado para pular a chamada por completo. O script mantém um Map de entradas { expires, data }, carrega-o de .amodb-cache.json ao iniciar e grava de volta no fim. Uma segunda execução sobre os mesmos ids não faz nenhuma requisição.
Passo 7: o programa completo
Salve isto como cards.mjs e execute node cards.mjs 550 603 27205.
Execute duas vezes. A primeira execução envia uma requisição por id, mais uma repetição se voltar um 429, e grava .amodb-cache.json. A segunda execução imprime os mesmos cartões e não envia nada. Apague o arquivo para forçar a atualização. Um id que não existe imprime 404: The resource you requested could not be found. e define o código de saída como 1, o que ajuda quando o script roda dentro de um job agendado.
O loop é sequencial de propósito. Rodar todos os ids de uma vez gastaria o orçamento por minuto em uma única rajada e levaria a maioria deles ao caminho do 429. Se precisar de velocidade, processe os ids em grupos de cinco e mantenha a lógica de repetição em 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 passos
- Transforme o loop em uma função pequena que devolve cartões e chame-a de um handler HTTP no seu próprio servidor. Mantenha a chave no servidor.
- Troque a lista de ids por resultados de em alta e imprima um cartão para cada um.
- Adicione
moodsousoundtracksaappend_to_response. Os moods (clima emocional) chegam comomoods.results[]comname,avgevaluesem uma escala de 0 a 10, emoods.sourceémeasuredouestimated. As trilhas sonoras chegam comosoundtracks.results[], vazio quando nenhum álbum é conhecido. Veja moods e trilhas sonoras. - Compare a mesma tarefa em Python ou leia o início rápido.