Nesta aula, vamos colocar em prática tudo o que aprendemos sobre JavaScript assíncrono construindo um projeto completo: um catálogo de filmes que consome dados de uma API pública. Você vai aprender a usar fetch para buscar dados, a renderizar dinamicamente o conteúdo no DOM e a lidar com erros e estados de carregamento de forma elegante. Este é um dos desafios mais comuns no dia a dia de uma pessoa desenvolvedora front-end: integrar uma aplicação com serviços externos de forma robusta e agradável para o usuário.

Vamos usar a API pública do Studio Ghibli (https://ghibliapi.vercel.app/), que fornece informações sobre filmes, personagens e locais dos famosos estúdios de animação. Ela é gratuita, não requer autenticação e retorna dados em JSON, perfeita para aprendizado. Ao final, você terá um projeto funcional que pode ser expandido e personalizado.

fetch

O fetch é uma API nativa do JavaScript para fazer requisições HTTP. Ela retorna uma Promise que resolve para um objeto Response. A partir desse objeto, podemos extrair os dados em formato JSON, texto ou outros formatos. O uso básico é:

fetch('https://ghibliapi.vercel.app/films')
  .then(response => response.json())
  .then(data => console.log(data))
  .catch(error => console.error('Erro:', error));

Note que response.json() também retorna uma Promise, por isso encadeamos outro .then(). O método fetch só rejeita a Promise em caso de falha de rede, não para erros de status HTTP (como 404 ou 500). Por isso, é importante verificar response.ok ou o status para tratar erros adequadamente.

Uma alternativa mais moderna é usar async/await, que torna o código mais legível:

async function getFilms() {
  try {
    const response = await fetch('https://ghibliapi.vercel.app/films');
    if (!response.ok) {
      throw new Error('Falha na requisição: ' + response.status);
    }
    const data = await response.json();
    return data;
  } catch (error) {
    console.error('Erro ao buscar filmes:', error);
  }
}

Nesse exemplo, usamos try/catch para capturar erros, tanto de rede quanto de status. A função getFilms retorna os dados ou undefined em caso de erro. No projeto, vamos usar essa abordagem para controlar melhor o fluxo.

Renderização dinâmica

Renderização dinâmica é o processo de criar elementos HTML e inseri-los no DOM a partir de dados obtidos em tempo de execução. No nosso projeto, vamos buscar a lista de filmes e criar cards para cada um, exibindo título, descrição e outras informações. Para isso, usamos manipulação do DOM, como document.createElement, ou templates com innerHTML.

Um padrão comum é criar uma função que recebe um objeto de filme e retorna o HTML do card. Depois, iteramos sobre a lista de filmes e concatenamos os HTMLs em uma string, que é inserida no container. Vamos ver um exemplo:

function criarCard(filme) {
  return `
    
${filme.title}
${filme.title}

${filme.description}

Diretor: ${filme.director}

Ano: ${filme.release_date}

`; } const container = document.getElementById('filmes-container'); container.innerHTML = filmes.map(criarCard).join('');

Aqui, usamos template literals para criar o HTML. É importante escapar caracteres especiais para evitar vulnerabilidades de XSS (Cross-Site Scripting). Nesse caso, a API é confiável, mas em produção devemos sanitizar os dados. Uma forma simples é substituir caracteres perigosos:

function escapeHtml(text) {
  const div = document.createElement('div');
  div.textContent = text;
  return div.innerHTML;
}

Assim, qualquer texto inserido no HTML via innerHTML é seguro. No projeto, vamos aplicar essa técnica para exibir descrições e títulos.

Tratamento de erros e loading

Uma boa experiência do usuário exige feedback visual durante o carregamento e quando ocorrem erros. Vamos implementar um estado de carregamento (loading) com um spinner ou mensagem, e uma mensagem de erro amigável caso a requisição falhe. O padrão é:

const loadingElement = document.getElementById('loading');
const errorElement = document.getElementById('error');
const container = document.getElementById('filmes-container');

async function carregarFilmes() {
  loadingElement.style.display = 'block';
  errorElement.style.display = 'none';
  container.innerHTML = '';

  try {
    const data = await getFilmes();
    if (data) {
      container.innerHTML = data.map(filme => criarCard(filme)).join('');
    } else {
      throw new Error('Nenhum dado recebido');
    }
  } catch (error) {
    errorElement.textContent = 'Ops! Não foi possível carregar os filmes. Tente novamente mais tarde.';
    errorElement.style.display = 'block';
  } finally {
    loadingElement.style.display = 'none';
  }
}

Usamos finally para garantir que o loading seja escondido, independente de sucesso ou erro. No HTML, teremos elementos como:



Aqui usamos o spinner do Bootstrap, mas você pode usar CSS puro ou ícones. O importante é comunicar ao usuário o que está acontecendo.

Projeto final

Vamos montar o projeto completo em um único arquivo HTML com CSS e JavaScript embutidos para simplificar. O código abaixo já inclui tudo que discutimos: busca com fetch, renderização dinâmica, tratamento de erros e loading. Copie e teste em seu navegador.




  
  
  Catálogo de Filmes - Studio Ghibli
  
  


  

Filmes do Studio Ghibli

Teste esse código em seu navegador. Você deve ver uma grade de cards com os filmes do Studio Ghibli. Se houver erro, a mensagem aparecerá. Experimente desligar a internet para ver o tratamento de erro.

Boas práticas e observações

Algumas boas práticas para consumo de APIs em JavaScript:

  • Sempre verifique response.ok antes de processar os dados.
  • Use try/catch/finally para controlar o fluxo assíncrono.
  • Sanitize dados vindos de APIs antes de inserir no DOM para evitar XSS.
  • Mantenha o código modular: separe funções de busca, renderização e tratamento.
  • Considere usar AbortController para cancelar requisições se necessário.
  • Para projetos maiores, use módulos ES6 para organizar o código.

Além disso, lembre-se de que, em produção, você pode usar Promise.all para buscar múltiplos recursos em paralelo, e bibliotecas como Axios para funcionalidades extras. Mas o fetch nativo é suficiente para a maioria dos casos.

Exercícios

  1. Modifique o projeto para exibir também os personagens de cada filme. Use a rota /people da API e relacione com os filmes pelo campo films.
  2. Adicione um campo de busca por título ou diretor. Ao digitar, filtre os filmes exibidos.
  3. Implemente um botão "Carregar mais" que busca a próxima página de resultados, se houver. (A API Ghibli não tem paginação, mas você pode simular com um limite.)
  4. Use AbortController para cancelar a requisição anterior quando o usuário digitar rapidamente no campo de busca.
  5. Refatore o código para usar módulos ES6: separe api.js, ui.js e main.js.

✓ Resposta: Para exibir personagens, você pode buscar /people e criar uma função que recebe um filme e retorna os personagens correspondentes. Exemplo:
async function getPersonagens() {
  const response = await fetch('https://ghibliapi.vercel.app/people');
  return await response.json();
}

async function carregarTudo() {
  const [filmes, personagens] = await Promise.all([getFilmes(), getPersonagens()]);
  filmes.forEach(filme => {
    filme.personagens = personagens.filter(p => p.films.includes(filme.id));
  });
  renderizarFilmes(filmes);
}
Depois, no card, liste os personagens.

✓ Resposta: Adicione um input de busca e um evento input que filtra os filmes já carregados. Exemplo:
document.getElementById('busca').addEventListener('input', (e) => {
  const termo = e.target.value.toLowerCase();
  const filtrados = todosFilmes.filter(f => f.title.toLowerCase().includes(termo) || f.director.toLowerCase().includes(termo));
  container.innerHTML = filtrados.map(criarCard).join('');
});
Guarde todos os filmes em uma variável global.

✓ Resposta: Como a API Ghibli não tem paginação, você pode simular paginação local: carregue todos os filmes, mas exiba apenas os primeiros 6. Ao clicar em "Carregar mais", aumente o limite. Exemplo:
let limite = 6;
function renderizar() {
  container.innerHTML = filmes.slice(0, limite).map(criarCard).join('');
}
document.getElementById('carregar-mais').addEventListener('click', () => {
  limite += 6;
  renderizar();
});

✓ Resposta: Use AbortController para cancelar a requisição anterior. Exemplo:
let controller;
input.addEventListener('input', async (e) => {
  if (controller) controller.abort();
  controller = new AbortController();
  try {
    const response = await fetch(url, { signal: controller.signal });
    // ...
  } catch (err) {
    if (err.name === 'AbortError') return;
    // tratar erro
  }
});

✓ Resposta: Crie arquivos separados e use import/export. Por exemplo, em api.js:
export async function getFilmes() { ... }
Em ui.js:
export function criarCard(filme) { ... }
Em main.js:
import { getFilmes } from './api.js';
import { criarCard } from './ui.js';
// ...
Lembre-se de carregar main.js com type="module".

Referências