# 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](https://amodb.dev/pt/pricing/), crie uma chave no painel da sua conta e coloque-a no ambiente:

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

O script lê `process.env.AMODB_KEY`, então a chave nunca vai parar no código-fonte nem no controle de versão.

## Passo 2: crie o projeto

Não há nada para instalar. Crie uma pasta e um `cards.mjs` vazio:

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

A versão deve mostrar `v20` ou superior. A extensão `.mjs` ativa módulos ES sem um `package.json`.

## Passo 3: envie a primeira requisição

Chame `GET /3/movie/{movie_id}` com a chave em um cabeçalho `Authorization` e um timeout, e confira o status antes de analisar o corpo.

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

## 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](https://amodb.dev/pt/credits/) e [avaliações](https://amodb.dev/pt/ratings/).

Em `ratings`, cada fonte é `{ value, votes }` ou `null`. O formatador de cartão abaixo ignora os nulos e junta o resto:

```js
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](https://amodb.dev/pt/rate-limits/) para as regras da janela e [erros](https://amodb.dev/pt/errors/) 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`.

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

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

## 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](https://amodb.dev/pt/trending-lists/) e imprima um cartão para cada um.
- Adicione `moods` ou `soundtracks` a `append_to_response`. Os moods (clima emocional) chegam como `moods.results[]` com `name`, `avg` e `values` em uma escala de 0 a 10, e `moods.source` é `measured` ou `estimated`. As trilhas sonoras chegam como `soundtracks.results[]`, vazio quando nenhum álbum é conhecido. Veja [moods](https://amodb.dev/pt/moods/) e [trilhas sonoras](https://amodb.dev/pt/soundtracks/).
- Compare a mesma tarefa em [Python](https://amodb.dev/pt/python-movie-api/) ou leia o [início rápido](https://amodb.dev/pt/quickstart/).

## Perguntas e respostas

### Este script precisa de algum pacote npm?

Não. O Node 20 ou mais recente traz fetch, URL e AbortSignal.timeout, então o script não tem dependências. A extensão .mjs permite usar import e await no nível superior. Execute com node cards.mjs seguido dos ids.

### Posso chamar o amodb direto do JavaScript do navegador?

Evite. Uma chave no código de front end fica visível para todo visitante e pode ser copiada e esgotada. Chame o amodb pelo Node ou pelo seu próprio servidor e envie à página só os campos de que ela precisa. As imagens não precisam de chave e carregam direto.

### Quantos ids posso processar em um minuto?

Uma requisição por id sem cache, porque créditos e avaliações chegam por append_to_response. O plano de 20 requisições por minuto cobre cerca de 20 ids novos por minuto, e os planos de 40 e 120 escalam isso. A repetição em 429 absorve picos curtos.

### Por que algumas avaliações faltam em um cartão?

Cada uma das cinco fontes de avaliação é um objeto com value e votes, ou null quando a fonte não é conhecida para o título. O script descarta os nulls, então um título pode mostrar quatro notas e outro pode não mostrar nenhuma.

### Onde o cache deve ficar em um serviço de longa duração?

Mantenha o Map na memória e dispense o arquivo. Use um TTL de 6 horas para detalhes, igual ao cache de borda, e apague as entradas expiradas na leitura para que o Map não cresça sem limite.

## Páginas relacionadas

- [API de detalhes de filmes](https://amodb.dev/pt/movies/): API de detalhes de filmes com caminhos e JSON do TMDB v3. Busque um filme por id e junte créditos, avaliações e moods com append_to_response, em 14 idiomas.
- [API de créditos](https://amodb.dev/pt/credits/): API de créditos com caminhos e JSON iguais aos do TMDB v3. Elenco e equipe técnica de filmes, séries, temporadas e episódios, com order, department e job.
- [Avaliações de filmes e séries](https://amodb.dev/pt/ratings/): API de avaliações de filmes com as notas do IMDb, Kinopoisk, Metacritic, Rotten Tomatoes e TMDB em uma só chamada, com null se a fonte for desconhecida.
- [Autenticação](https://amodb.dev/pt/authentication/): Como enviar sua chave do amodb: api_key na consulta, um cabeçalho Authorization Bearer ou apikey nas chamadas OMDb. Formato, armazenamento, rotação e erros 401.
- [Limites de requisições](https://amodb.dev/pt/rate-limits/): O amodb limita cada chave por requisições por minuto em uma janela de 60 segundos: 20, 40 ou 120 por plano. Leia os cabeçalhos e trate o 429 com Retry-After.
- [Tutorial de API de filmes em Python](https://amodb.dev/pt/python-movie-api/): Crie uma ferramenta de linha de comando em Python que busca um filme e imprime ano, elenco, nota do IMDb e pôster, com nova tentativa no 429 e cache em disco.

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