Projeto: consumindo uma API pública
LidoNesta aula, você aprenderá a consumir uma API pública usando fetch, a renderizar dados dinamicamente no DOM e a tratar erros e estados de carregamento de forma profissional. Ao final, você terá um projeto completo de catálogo de filmes com interface amigável.
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.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.okantes de processar os dados. - Use
try/catch/finallypara 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
AbortControllerpara 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
- Modifique o projeto para exibir também os personagens de cada filme. Use a rota
/peopleda API e relacione com os filmes pelo campofilms. - Adicione um campo de busca por título ou diretor. Ao digitar, filtre os filmes exibidos.
- 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.)
- Use
AbortControllerpara cancelar a requisição anterior quando o usuário digitar rapidamente no campo de busca. - Refatore o código para usar módulos ES6: separe
api.js,ui.jsemain.js.
/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.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.let limite = 6;
function renderizar() {
container.innerHTML = filmes.slice(0, limite).map(criarCard).join('');
}
document.getElementById('carregar-mais').addEventListener('click', () => {
limite += 6;
renderizar();
});
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
}
});
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".