Pular para o conteúdo principal

Aula 13: Dados da API — carregamento, erro e renderização

A aula 12 construiu o painel do acervo sobre dados fixos, declarados em lib/livros.ts no formato que a API do Módulo 2 devolve. Esta aula troca a origem desses dados: o cliente web passa a consultar a API, e o painel continua o mesmo, sem nenhuma alteração.

O que muda é tudo o que está em volta. Uma chamada pela rede demora, pode falhar e pode responder que o registro não existe; o framework precisa saber se a página deve ser montada uma vez, no build, ou a cada requisição; e o navegador passa a impor regras que não valiam enquanto os dados moravam no mesmo processo. São quatro decisões — de que lado a chamada acontece, o que mostrar em cada desfecho, quando a página é montada e quem autoriza o navegador a ler a API —, e são elas o conteúdo da aula.

Ao fim da aula, /livros e /livros/[id] leem a API pelo servidor, com esqueleto de carregamento e tela de falha, e uma rota nova, /livros/busca, consulta a API pelo navegador a cada tecla digitada.

O que vem depoisOnde
Formulários, envio de dados à API, paginação e cacheAula 14 — Consumo de serviços no frontend
Login, sessão, rotas protegidas e publicaçãoAula 15 — Sessão e publicação
O mesmo serviço consumido por outro clienteMódulo 4 — Flutter

Objetivos​

Ao final desta aula, você deve ser capaz de:

  • Comparar a chamada à API feita pelo componente de servidor com a feita pelo navegador, e decidir qual usar diante de um requisito.
  • Implementar o acesso à API em um módulo próprio, tratando separadamente resposta com dados, registro inexistente e falha.
  • Explicar por que uma página que chama a API pode ser gerada uma única vez no build, e prever o efeito disso sobre os dados exibidos.
  • Justificar a escolha entre rota estática e dinâmica, e aplicar connection() quando a página depende do momento da requisição.
  • Descrever como layout.tsx, error.tsx, loading.tsx e page.tsx de um segmento se aninham, e identificar o que continua na tela em cada situação.
  • Implementar uma busca no navegador com useEffect, modelando os estados da requisição com um tipo união.
  • Explicar o que é CORS, quem o aplica e por que a chamada feita pelo servidor não depende dele, e distinguir a requisição simples da requisição com preflight.
  • Diagnosticar três defeitos que não se manifestam no ponto em que o aluno os procura: o acervo congelado no build, o bloqueio de CORS e a resposta atrasada que sobrescreve a mais nova.

Ambiente sugerido​

Pré-requisitos: Fundamentos de React (aula 12), com o laboratório daquela aula concluído e rodando, e a API construída no Módulo 2 funcionando com o banco populado pelo seed. Nesta aula, o consumo usa as rotas públicas da API; autenticação e sessão ficam fora deste laboratório. De Assincronismo em TypeScript (Extra A), revise async/await, a Promise que rejeita e a condição de corrida entre dois await.

O laboratório é executável e parte do projeto da aula 12. Os trechos nas partes conceituais são somente leitura; trechos parciais do laboratório indicam onde devem ser inseridos.

Pacotes de checkpoint​

Cada checkpoint do roteiro tem um .zip com o frontend exatamente naquele ponto: código-fonte completo, sem node_modules/. Servem para quem perdeu parte da aula, quer comparar o próprio projeto com a referência, ou prefere conferir um trecho adiante sem digitar tudo o que vem antes. A API é sempre a mesma dos passos 1 e 2 — construída no Módulo 2, assumida completa e no ar; os pacotes não mexem nela.

Para usar um checkpoint:

  1. Baixe e descompacte o .zip.
  2. Dentro da pasta, npm install.
  3. Copie .env.example para .env.local (os valores padrão já apontam para http://localhost:3000, a porta da API).
  4. Com a API no ar, npm run dev — o cliente sobe em http://localhost:3001.
CheckpointDepois doO que já funciona
aula-13-biblioteca-web-checkpoint-1.zipPasso 5/livros e /livros/[id] lendo a API pelo servidor, sem connection(), sem layout aninhado, sem busca
aula-13-biblioteca-web-checkpoint-2.zipPasso 7/livros com connection(): dinâmica, e o build já não depende da API
aula-13-biblioteca-web-checkpoint-3.zipPasso 10layout aninhado do segmento, loading.tsx e error.tsx
aula-13-biblioteca-web-checkpoint-4.zipPasso 13busca por autor pelo navegador — o laboratório completo
Não construiu a API do Módulo 2? Baixe-a pronta

O pacote biblioteca-api-sem-autenticacao.zip traz o backend completo das aulas 4 a 9 — acervo, relacionamentos, paginação, erros padronizados e seed —, sem a autenticação da aula 10. Descompacte, siga o README.md do pacote (npm install, migration e npx prisma db seed) e suba a API em http://localhost:3000 antes de iniciar o laboratório desta aula.

FerramentaVersãoObservação
Node.js22 LTS ou superiora mesma das aulas anteriores
Next.js16.xexemplos verificados com 16.3.4
React19.2.xverificado com 19.2.8
TypeScript5.xmínimo 5.1
API do Módulo 2API construída nas aulas anterioresem http://localhost:3000, com o seed aplicado
PostgreSQL16 ou superioro mesmo das aulas 7 a 10
NavegadorChrome, Edge, Firefox ou Safari recenteso console e a aba de rede das ferramentas de desenvolvedor são usados nos passos 11 a 13

A partir desta aula, os dois projetos rodam ao mesmo tempo: a API na porta 3000 e o cliente web na 3001.

Material de terceiros sobre busca de dados no Next.js envelhece rápido

Três sinais de que um tutorial não vale para esta disciplina: getServerSideProps ou getStaticProps (é o Pages Router, não o App Router); a afirmação de que "fetch é guardado em cache por padrão" (valia até o Next.js 14; desde o 15, não é); e error.tsx recebendo apenas reset (na versão usada aqui, a função recomendada é retry). Confira sempre se a página se refere à versão 16.


Conteúdo​

O que as aulas anteriores deixaram em aberto​

Pergunta deixadaOnde é respondida
Aula 11: o que fazem loading.tsx e error.tsx?Partes 5 e 6
Aula 11: como pré-renderizar os caminhos conhecidos de /livros/[id]?Parte 4
Aula 12: quando uma rota deixa de poder ser estática?Parte 4, e os passos 6 e 7 do laboratório
Aula 12: onde entra o useEffect que ficou de fora?Parte 7

Parte 1 — Os dois caminhos até a API​

Um cliente web construído com Next.js tem dois lugares de onde pode chamar a API, e eles não são equivalentes. A Figura 1 mostra os dois lado a lado.

Diagrama com três colunas: à esquerda o navegador, em localhost:3001; no meio o servidor Next.js; à direita a API NestJS, em localhost:3000, ligada ao PostgreSQL. Na faixa de cima, rotulada pelo servidor, a rota /livros: o navegador pede GET /livros ao servidor Next.js, que chama a API com fetch, recebe JSON e devolve ao navegador HTML pronto. Na faixa de baixo, rotulada pelo navegador, a rota /livros/busca: o navegador chama a API diretamente, enviando o cabeçalho Origin com http://localhost:3001, e recebe JSON acompanhado, ou não, do cabeçalho Access-Control-Allow-Origin. Uma caixa âmbar embaixo explica que quem confere esse cabeçalho é o navegador, e que na faixa de cima não há navegador entre o Next.js e a API.
O mesmo dado pode chegar à tela por dois caminhos. Só o de baixo passa pelo navegador — e só ele está sujeito ao CORS.

Pelo servidor. O componente de servidor chama a API, espera a resposta e monta o HTML com os dados já incluídos. O navegador recebe a página pronta e não sabe que a API existe. É o caminho que a aula 11 anunciou quando disse que o servidor lê os dados e o cliente cuida da interação.

Pelo navegador. Um componente de cliente, já na tela, chama a API diretamente. O servidor Next.js não participa. É o caminho de quem depende de algo que só existe no navegador — o texto que o leitor está digitando, por exemplo.

Pelo servidorPelo navegador
Quem executa o fetchcomponente de servidorcomponente de cliente, depois de a tela aparecer
O que o leitor recebe primeiroHTML com os dadosHTML sem os dados, e depois uma segunda espera
Endereço da APIAPI_URL, visível só no servidorNEXT_PUBLIC_API_URL, copiado para o JavaScript do cliente
Sujeito a CORSnãosim
Serve parao que a página precisa mostrar ao abriro que depende de interação

O padrão desta disciplina, e do próprio Next.js, é o primeiro. O segundo existe para os casos em que não há como saber no servidor o que buscar, e esta aula o constrói de propósito, para que a diferença seja vista e não apenas descrita. A Parte 10 volta à comparação com o que o laboratório mediu, e propõe um roteiro para escolher entre os dois.

Duas variáveis para o mesmo endereço

Em desenvolvimento, API_URL e NEXT_PUBLIC_API_URL valem o mesmo. Elas existem separadas porque, numa publicação, o servidor costuma falar com a API por um endereço interno, que o navegador do leitor não alcança. A diferença de nome não é convenção: o Next.js só copia para o JavaScript do cliente as variáveis com o prefixo NEXT_PUBLIC_, e faz isso no momento do build — trocar o valor depois exige um build novo.


Parte 2 — A chamada no servidor​

O acesso à API fica num módulo só, lib/livros.ts, que já existia. As funções mantêm os nomes — listarLivros e buscarLivro —, mas passam a ser assíncronas:

lib/livros.ts (trecho)
const API_URL = process.env.API_URL ?? "http://localhost:3000";

export async function listarLivros(): Promise<Livro[]> {
const resposta = await fetch(`${API_URL}/livros?tamanho=100`);

if (!resposta.ok) {
throw new Error(`GET /livros respondeu ${resposta.status}`);
}

const pagina: Pagina<Livro> = await resposta.json();
return pagina.dados;
}

Três detalhes do trecho vêm diretamente do contrato do Módulo 2:

  • GET /livros devolve um envelope, { dados, pagina, tamanho, total, totalDePaginas }, definido na aula 6. A função devolve só dados, e é por isso que os componentes não percebem a troca.
  • tamanho=100 é o teto que o ConsultarLivrosDto aceita. O acervo do seed cabe numa página; paginar na interface é assunto da aula 14.
  • O tipo Autor muda num ponto: nacionalidade passa a ser string | null, porque no schema da aula 8 o campo é opcional. O acervo fixo declarava string porque todos os autores tinham o campo; o tipo agora diz o que o contrato permite, e não o que os dados de exemplo tinham.

Quem chama uma função assíncrona precisa esperar por ela. A página vira async e ganha um await:

app/livros/page.tsx (trecho)
export default async function Page() {
const livros = await listarLivros();

return (
<>
<h1 className="text-2xl font-semibold">Acervo</h1>
<PainelAcervo livros={livros} />
</>
);
}

Esquecer o await não passa despercebido, e convém entender por quê. Sem ele, livros é uma Promise, e o TypeScript recusa entregá-la ao painel: Type 'Promise<Livro[]>' is missing the following properties from type 'Livro[]'. Na página de detalhe o risco é mais sutil: if (!livro) nunca seria verdadeiro, porque uma Promise é sempre um valor verdadeiro — o teste de ausência passaria sem que o livro existisse. Também aqui é o TypeScript que impede o erro, ao recusar livro.titulo sobre uma Promise.

O tipo descreve o que o cliente lê, não o que ele recebe

A API devolve campos que o tipo Livro não declara — autorId, criadoEm, atualizadoEm. O TypeScript não os remove: ele só confere o que o código lê. E, como o PainelAcervo é componente de cliente, tudo o que chega a ele por props é serializado dentro do HTML, inclusive esses campos. Se um campo não deve chegar ao navegador, quem precisa deixar de enviá-lo é a API — ou a função de acesso, escolhendo os campos antes de devolver.


Parte 3 — Os três desfechos de uma chamada​

Uma requisição à API tem três desfechos possíveis, e cada um pede uma resposta diferente da interface:

DesfechoComo chega ao códigoO que a interface mostra
Dadosresposta.ok verdadeiroa página
O registro não existeresposta.status === 404not-found.tsx, via notFound()
Falhafetch rejeita, ou qualquer outro status fora de 2xxerror.tsx, via exceção lançada

A última linha é a que mais gera confusão. O fetch só rejeita quando não há resposta nenhuma — API fora do ar, porta errada, rede caída. Um 500 é uma resposta; chega ao código com ok falso, e quem transforma isso em falha é o throw da função de acesso. Sem ele, o código tentaria ler o corpo de um erro como se fosse um livro.

A função de detalhe mostra a separação entre os dois últimos desfechos:

lib/livros.ts (trecho)
export async function buscarLivro(id: number): Promise<Livro | undefined> {
const resposta = await fetch(`${API_URL}/livros/${id}`);

if (resposta.status === 404) {
return undefined;
}
if (!resposta.ok) {
throw new Error(`GET /livros/${id} respondeu ${resposta.status}`);
}

return resposta.json();
}

O 404 não é falha: é uma das respostas previstas no contrato, e quem decide o que mostrar é a página. A função devolve undefined, e a página chama notFound(), como já fazia na aula 11.

Há um quarto caso, que a página resolve antes de chamar a API: /livros/abc. A verificação de formato que a aula 11 escreveu continua, e agora tem uma razão a mais. Sem ela, o ParseIntPipe da API responderia 400, e a função trataria isso como falha — o leitor veria "não foi possível carregar" para um endereço que simplesmente não existe:

app/livros/[id]/page.tsx (trecho)
const { id } = await params;

if (!/^[1-9]\d*$/.test(id) || !Number.isSafeInteger(Number(id))) {
notFound();
}

const livro = await buscarLivro(Number(id));

if (!livro) {
notFound();
}
No seu projeto

A pergunta "o que é ausência e o que é falha" é do padrão; a resposta é do domínio. Aqui, livro inexistente é ausência. No seu domínio, liste as rotas de leitura da sua API e marque, para cada status que ela pode devolver, se a interface deve mostrar "não encontrado" ou "não foi possível carregar".


Parte 4 — Estático ou dinâmico, agora que há rede no meio​

A aula 11 mostrou o mapa de rotas do next build, com ○ para o que é gerado no build e ƒ para o que é montado a cada requisição. A aula 12 terminou perguntando quando uma rota deixa de poder ser estática. Com a API no meio, a resposta a essa pergunta é pouco intuitiva.

A regra, na configuração padrão do Next.js 16: um fetch sem opção de cache, alcançado antes de qualquer API que dependa da requisição, é executado uma vez, durante o build, e o resultado vai para o HTML gerado. A página de listagem não lê cabeçalho, cookie nem parâmetro de URL — nada que dependa da requisição. Então:

  • /livros continua marcada com ○, e o acervo que ela mostra é o que a API devolveu no momento do build. Alterar um título na API não muda a página; desligar a API também não — a página continua respondendo 200 com os dados antigos;
  • o build passa a depender da API. Com ela fora do ar, um build feito do zero falha: Error occurred prerendering page "/livros", seguido de TypeError: fetch failed e da causa, connect ECONNREFUSED 127.0.0.1:3000. Se a pasta .next de um build anterior ainda existir, ele passa, reaproveitando o que ficou guardado — e o defeito só aparece na primeira vez que o projeto for construído numa máquina limpa, como a de publicação.

Os dois efeitos são medidos nos passos 6 e 7 do laboratório. Nenhuma delas é um defeito do framework: é a otimização da aula 11 aplicada a uma página cujos dados mudam. Quem sabe se eles mudam é quem escreve a página, e é preciso dizer isso ao framework.

MecanismoO que declaraEfeito no mapa de rotas
nadaos dados podem ser lidos uma vez, no build○
await connection() antes da chamadaa página depende do momento da requisiçãoƒ
ler searchParams, cookies() ou headers()idem, como consequência de usar a requisiçãoƒ
fetch(url, { cache: "no-store" })esta chamada nunca é reaproveitadaƒ
fetch(url, { next: { revalidate: 60 } })o resultado vale por 60 segundos○, regenerada depois do prazo

O laboratório usa a segunda linha, connection(), importada de next/server:

app/livros/page.tsx (trecho)
import { connection } from "next/server";

export default async function Page() {
await connection();

const livros = await listarLivros();
// ...
}

Com ela, as duas consequências anteriores desaparecem juntas: a página mostra o que a API tem no momento em que o leitor pede, e o build deixa de chamar a API — ele passa mesmo com a API desligada, porque não há mais nada a pré-renderizar em /livros.

A escolha é deliberada. connection() fica na página, que é quem decide de que a tela depende; a opção cache ficaria na função de acesso, que não sabe quem a chama. A última linha da tabela, a revalidação por prazo, é a resposta certa para muitos catálogos e é assunto da aula 14, junto com o resto do cache.

E a página de detalhe? /livros/[id] já era ƒ na aula 11, porque nenhum id foi enumerado para o build. A aula 11 prometeu mostrar como enumerá-los; é assim:

app/livros/[id]/page.tsx — somente leitura, fora do laboratório
export async function generateStaticParams() {
const livros = await listarLivros();
return livros.map((livro) => ({ id: String(livro.id) }));
}

Com essa função, o build chama a API, gera uma página por livro e o mapa de rotas passa a listar ● /livros/1, ● /livros/2 e ● /livros/3, com o símbolo de páginas geradas a partir de parâmetros. O laboratório não a adota, pelo mesmo motivo da listagem: o acervo muda, e cada página gerada congelaria o livro como estava no build. Ela é a escolha certa quando o conjunto de registros é conhecido e estável — páginas de documentação, por exemplo.


Parte 5 — Enquanto não chega: loading.tsx​

Uma página dinâmica que aguarda a API mantém a tela anterior visível até a resposta chegar. O arquivo especial loading.tsx resolve isso: o Next.js envolve a página do segmento numa fronteira de Suspense do React e, enquanto ela espera, mostra o conteúdo de loading.tsx no lugar dela.

app/livros/loading.tsx (trecho)
export default function Loading() {
return (
<div aria-busy="true" aria-live="polite">
<p className="text-sm opacity-80">Carregando o acervo…</p>
{/* três cartões vazios, pulsando */}
</div>
);
}

O que não é substituído importa tanto quanto o que é. A Figura 2 mostra como os arquivos especiais de um segmento se aninham.

Cinco caixas aninhadas, da mais externa para a mais interna: app/layout.tsx, com cabeçalho e rodapé; livros/layout.tsx, com a navegação do acervo; error.tsx, a fronteira de erro, em vermelho; loading.tsx, a fronteira de Suspense, em âmbar; e page.tsx, que executa await listarLivros. À direita, quatro anotações ligadas às caixas: quando os dados chegam, a página substitui o esqueleto; enquanto espera, o esqueleto ocupa o lugar da página; quando falha, a mensagem de erro ocupa o lugar do que está dentro e o que está fora continua; os dois layouts ficam na tela nos três casos. Uma nota embaixo registra que error.tsx não envolve o layout do próprio segmento.
Cada arquivo especial é uma fronteira em volta do que está dentro dele. O layout do segmento fica fora das duas fronteiras, e por isso a navegação do acervo nunca some.

É por isso que o laboratório cria um layout aninhado, app/livros/layout.tsx, com a navegação entre "Todos os títulos" e "Buscar por autor". Ele envolve as três rotas do segmento, não é desmontado quando o leitor navega entre elas e, por estar fora da fronteira de Suspense, aparece imediatamente — só a área da página dá lugar ao esqueleto.

Com loading.tsx, o 404 passa a chegar com status 200

A aula 11 avisou que, depois de iniciado o streaming, o status pode permanecer 200. Com o loading.tsx, é o que passa a acontecer: um livro inexistente responde 200, e a tela continua sendo "Página não encontrada". Não é defeito. Para mostrar o esqueleto, o servidor precisa começar a enviar a resposta antes de a página terminar, e o status vai no começo da resposta. Quando notFound() acontece, o 200 já foi enviado. O Next.js acrescenta ao HTML <meta name="robots" content="noindex">, para que buscadores não indexem a página. Se a sua aplicação precisar do status 404 de verdade — para monitoramento, por exemplo —, a verificação tem de acontecer antes de a resposta começar, fora da fronteira de Suspense.


Parte 6 — Quando falha: error.tsx​

O error.tsx é a fronteira de erro do segmento. Uma exceção lançada dentro dele — pela página, pela função de acesso, por qualquer componente — é capturada, e o conteúdo de error.tsx ocupa o lugar do que estava dentro da fronteira.

app/livros/error.tsx (trecho)
"use client";

export default function Erro({
error,
retry,
}: {
error: Error & { digest?: string };
retry: () => void;
}) {
return (
<div role="alert" className="rounded border border-red-600/40 p-4">
<p className="font-semibold">Não foi possível carregar o acervo.</p>
{/* ... */}
<button type="button" onClick={() => retry()}>
Tentar de novo
</button>
</div>
);
}

Três aspectos desse arquivo não dependem de escolha do autor:

  • "use client" é obrigatório. A fronteira precisa responder ao clique em "Tentar de novo", e também captura erros que só acontecem no navegador.
  • retry pede de novo. Ela refaz a busca dos dados e renderiza outra vez o que estava dentro da fronteira. Se a API voltou, a página aparece. Existe também reset, que só renderiza de novo, sem buscar — raramente é o que se quer quando a causa é a rede.
  • A mensagem original não chega ao navegador em produção. Em desenvolvimento, error.message traz o texto do erro. Em produção, o Next.js o substitui por um texto genérico e entrega só error.digest, um identificador que aparece no terminal do servidor ao lado do erro verdadeiro. É proteção, não descuido: a mensagem de um erro de servidor pode conter endereço interno, trecho de SQL ou caminho de arquivo. O laboratório mostra o digest na tela, e o passo 10 confere que o mesmo número aparece no terminal.

Parte 7 — Buscar pelo navegador: useEffect​

A rota /livros/busca consulta GET /livros?autor=… a cada tecla. Aqui o servidor não pode fazer a consulta: ele não sabe o que o leitor vai digitar. A chamada precisa acontecer no navegador, depois de a tela existir, e em resposta a uma mudança de estado — o texto do campo.

É para isso que existe o Effect. Um useEffect declara um trecho de código que sincroniza o componente com algo que está fora do React — aqui, a API — e que roda depois de a tela ser pintada, de novo sempre que um dos valores da lista de dependências mudar:

app/components/BuscaPorAutor.tsx (trecho)
useEffect(() => {
if (termoLimpo === "") {
return;
}

let ignorar = false;

const url = `${API_PUBLICA}/livros?autor=${encodeURIComponent(termoLimpo)}`;

fetch(url)
.then((resposta) => {
if (!resposta.ok) {
throw new Error(`GET /livros respondeu ${resposta.status}`);
}
return resposta.json() as Promise<Pagina<Livro>>;
})
.then((pagina) => {
if (!ignorar) {
setBusca({ situacao: "pronta", livros: pagina.dados });
}
})
.catch(() => {
if (!ignorar) {
setBusca({ situacao: "erro" });
}
});

return () => {
ignorar = true;
};
}, [termoLimpo]);

Duas peças do trecho merecem uma explicação mínima antes de seguir, para não ficarem soltas até a Parte 9. A função devolvida no fim, () => { ignorar = true; }, é a limpeza do Effect: o React a chama antes de rodar o Effect de novo, e ao desmontar o componente. Ela marca ignorar como true, e cada if (!ignorar) descarta uma resposta que chegou depois de a execução que a pediu já ter sido substituída. Por que isso é necessário — e o que dá errado sem ele — é o assunto da Parte 9, com uma figura da corrida entre respostas. Antes dela, duas decisões de modelagem que vêm diretamente da aula 12.

Os três desfechos viram um tipo, não três variáveis. O servidor tinha loading.tsx e error.tsx; no navegador, o componente precisa representar os mesmos três desfechos sozinho. Três estados independentes — carregando, erro, livros — permitiriam combinações que não existem, como "carregando e com erro". Um tipo união diz ao TypeScript que a busca está em exatamente uma situação:

app/components/BuscaPorAutor.tsx (trecho)
type Busca =
| { situacao: "carregando" }
| { situacao: "erro" }
| { situacao: "pronta"; livros: Livro[] };

Dentro do ramo situacao === "pronta", e só nele, o TypeScript deixa ler busca.livros.

"Carregando" começa no evento, não no Effect. É a tecla que dispara a busca nova, e é nela que a tela deve reagir. O handler do campo atualiza o termo e marca a busca como em andamento; o Effect só trata da comunicação com a API. A alternativa — chamar setBusca no corpo do Effect, antes do fetch — custa uma renderização a mais a cada tecla, e o ESLint do projeto a recusa com a regra react-hooks/set-state-in-effect. E o caso do campo vazio não é um estado: é derivado do termo, como na Parte 6 da aula 12.

app/components/BuscaPorAutor.tsx (trecho)
function mudarTermo(novoTermo: string) {
setTermo(novoTermo);
if (novoTermo.trim() !== termoLimpo) {
setBusca({ situacao: "carregando" });
}
}

A comparação existe por um motivo concreto: se o leitor digitar só um espaço no fim, termoLimpo não muda, o Effect não roda de novo — e a tela ficaria presa em "Buscando…".

useEffect não é a forma de buscar dados de uma página

Se o dado pode ser conhecido no servidor, ele é buscado no servidor, como nas Partes 2 a 6. O Effect entra aqui porque o que se busca depende de algo que só existe no navegador. Em aplicações com muitas buscas desse tipo, bibliotecas como SWR e TanStack Query cuidam de cache, repetição e cancelamento; elas fazem o mesmo que este componente, com mais recursos, e o componente escrito à mão é o que permite entender o que elas resolvem.

Carregar algo só ao montar: o array vazio​

Nem todo Effect reage a uma tecla. Quando o que falta é só "buscar algo assim que o componente aparece na tela" — um componente de cliente que precise preencher sozinho um seletor, por exemplo, em vez de recebê-lo pronto do servidor por props, como faz o formulário da aula 14 —, a lista de dependências fica vazia:

exemplo — fora do laboratório
"use client";

import { useEffect, useState } from "react";
import type { Autor } from "@/lib/livros";

const API_PUBLICA = process.env.NEXT_PUBLIC_API_URL ?? "http://localhost:3000";

export default function SeletorDeAutor() {
const [autores, setAutores] = useState<Autor[]>([]);

useEffect(() => {
let ignorar = false;

fetch(`${API_PUBLICA}/autores`)
.then((resposta) => resposta.json())
.then((dados: Autor[]) => {
if (!ignorar) {
setAutores(dados);
}
});

return () => {
ignorar = true;
};
}, []);

// ...
}

[] diz ao React que o Effect não depende de nada que mude depois da primeira renderização: ele roda uma vez, assim que o componente aparece na tela, e só roda de novo se o componente for desmontado e montado outra vez. É o equivalente, no navegador, ao que a Parte 2 faz no servidor — buscar o dado antes de a tela existir —, só que aqui a tela já existe e o dado chega depois dela.

A limpeza continua valendo, mesmo sem tecla nenhuma envolvida: é ela quem explica a chamada dupla do aviso a seguir.

Por que a API recebe duas chamadas ao montar, mesmo com []

Abra uma tela assim em desenvolvimento, com a aba de rede aberta: /autores é chamada duas vezes, não uma — e a chamada aparece em dobro também no terminal do npm run start:dev da API. Não é defeito do código nem do []. É o modo estrito do React, ligado por padrão no Next.js e ativo só em desenvolvimento, simulando de propósito uma desmontagem seguida de remontagem — algo que acontece de verdade em várias navegações do App Router:

  1. o componente monta; o Effect roda e dispara o fetch da chamada 1;
  2. o modo estrito desmonta o componente imediatamente; a limpeza roda, e o ignorar desta execução vira true;
  3. o modo estrito monta o componente de novo; o Effect roda outra vez, com um ignorar novo, igual a false, e dispara o fetch da chamada 2;
  4. quando a resposta da chamada 1 chega, cai no if (!ignorar) da execução já descartada e não atualiza nada; só a resposta da chamada 2 preenche autores.

Sem a limpeza, as duas chamadas ainda aconteceriam, mas qualquer uma das duas respostas poderia vencer, dependendo de qual demorasse mais — a mesma corrida da Parte 9, só que entre duas execuções do mesmo Effect, provocadas pelo próprio React, e não por duas teclas do usuário.

Em produção (npm run build && npm run start), o modo estrito não roda, e a chamada acontece uma única vez. O [] não evita a chamada dupla em desenvolvimento — isso é esperado e documentado pelo React; a limpeza é o que evita que ela vire um defeito visível na tela.


Parte 8 — CORS: quem autoriza o navegador a ler a API​

Com a busca pronta, a primeira tecla produz "Não foi possível consultar a API.", e o console do navegador explica:

Access to fetch at 'http://localhost:3000/livros?autor=clarice' from origin
'http://localhost:3001' has been blocked by CORS policy: No
'Access-Control-Allow-Origin' header is present on the requested resource.

A mensagem envolve três conceitos — origem, política e um cabeçalho —, e entender cada um é o que permite corrigir o erro, em vez de tentar alternativas até que ele desapareça. Esta parte as apresenta na ordem em que o navegador as usa.

Origem​

Uma origem é o trio esquema, host e porta de um endereço. O caminho não entra na comparação. Tomando como referência a página do laboratório, http://localhost:3001:

EndereçoMesma origem?Por quê
http://localhost:3001/livros/buscasimsó o caminho é diferente
http://localhost:3000/livrosnãoa porta é outra
https://localhost:3001nãoo esquema é outro
http://127.0.0.1:3001nãoo host é outro: o navegador compara o texto, sem resolver o nome

A última linha costuma surpreender e corresponde a um erro frequente: o mesmo cliente aberto por 127.0.0.1 tem outra origem, e a liberação feita para localhost não vale para ele.

A política de mesma origem​

A regra de base não é o CORS: é a política de mesma origem, que todo navegador aplica. Ela é mais estreita do que o nome sugere.

  • O que ela permite. Uma página pode enviar requisições e incorporar recursos de outras origens. Imagens, folhas de estilo, scripts e envio de formulários sempre funcionaram assim na web.
  • O que ela proíbe. O JavaScript de uma página não pode ler a resposta de uma requisição feita a outra origem.

O motivo são as credenciais. O navegador anexa sozinho os cookies de um site a cada requisição feita a ele. Sem a política, qualquer página aberta poderia chamar o webmail ou o banco do leitor, com a sessão dele, e ler o resultado. Quem guarda as credenciais é o navegador — e por isso quem aplica a regra é ele.

CORS: a API abre uma exceção​

O CORS (Cross-Origin Resource Sharing) é o mecanismo pelo qual um servidor diz ao navegador quais outras origens podem ler as suas respostas. É um diálogo feito só de cabeçalhos:

  1. o navegador acrescenta à requisição o cabeçalho Origin, com a origem da página — o JavaScript não consegue alterá-lo;
  2. o servidor responde e, se quiser liberar, inclui Access-Control-Allow-Origin com uma origem;
  3. o navegador compara os dois valores. Se coincidem, entrega a resposta ao código. Se não coincidem, ou se o cabeçalho não veio, o fetch rejeita com TypeError: Failed to fetch.

O código não recebe nem o status nem o corpo, e não tem como saber o motivo da falha: a explicação vai só para o console, para quem está desenvolvendo. É proposital — se o código pudesse ler o motivo, poderia sondar a outra origem por ele.

O detalhe que mais desfaz confusão está na Figura 1: quem aplica a regra é o navegador. A requisição bloqueada chegou à API e foi respondida — o passo 11 confere isso com curl, que não é navegador e recebe os dados normalmente. Por isso a chamada da Parte 2, feita pelo servidor Next.js, nunca foi afetada: entre o servidor e a API não há navegador para conferir nada. E por isso CORS não é proteção da API: ele protege o leitor, não o serviço. Autenticação e autorização são mecanismos separados, estudados na aula 10 e deixados fora do escopo deste laboratório.

Requisição simples e requisição com preflight​

A frase "a requisição chegou à API" vale para a busca por autor, mas não para toda requisição. O navegador separa as requisições feitas a outra origem em dois tipos, e a Figura 3 mostra os dois lado a lado.

Dois diagramas de sequência lado a lado, com o tempo correndo para baixo, entre o navegador em localhost:3001 e a API em localhost:3000. À esquerda, a requisição simples: o navegador envia GET /livros?autor=clarice com o cabeçalho Origin; a API executa e responde 200 com JSON e, talvez, Access-Control-Allow-Origin; só então o navegador confere se Allow-Origin é igual à Origin — se sim, entrega a resposta ao código; se não, descarta e o fetch rejeita. À direita, a requisição com preflight, um POST /livros com Content-Type application/json: o navegador envia primeiro OPTIONS /livros com Access-Control-Request-Method POST e Access-Control-Request-Headers content-type; a API responde 204 sem corpo, com Allow-Origin, Allow-Methods e Allow-Headers; o navegador confere origem, método e cabeçalhos — se não estiverem autorizados, o POST nunca sai; se estiverem, o POST é enviado, a API responde 201 ou 400 com Allow-Origin e a resposta é entregue ao código. Uma nota embaixo resume quais requisições são simples.
Na requisição simples, a API executa primeiro e o navegador confere depois. Na requisição com preflight, o navegador pergunta antes, e a requisição real só sai se a resposta autorizar.

Requisição simples é a que atende a todas estas condições:

  • método GET, HEAD ou POST;
  • só cabeçalhos da lista segura escritos pelo código — Accept, Accept-Language, Content-Language e Content-Type;
  • Content-Type, se houver, igual a application/x-www-form-urlencoded, multipart/form-data ou text/plain.

Ela sai direto, e a conferência acontece depois da resposta. A API, portanto, executa a requisição de qualquer forma. O navegador não solicita autorização porque um formulário HTML já podia enviar exatamente essa requisição antes de o CORS existir: liberá-la não concede à página nenhuma capacidade nova.

Requisição com preflight é todo o resto. Antes dela, o navegador envia por conta própria uma pergunta, com o método OPTIONS:

OPTIONS /livros HTTP/1.1
Origin: http://localhost:3001
Access-Control-Request-Method: POST
Access-Control-Request-Headers: content-type

A resposta precisa autorizar a origem, o método e cada cabeçalho pedido. Só então a requisição real é enviada. Se a resposta não autorizar, a requisição real nunca sai — não há o que procurar na API, porque nada chegou a ela. O preflight existe por causa dos servidores escritos antes do CORS, que contavam com o fato de nenhum navegador mandar um DELETE ou um JSON vindos de outra origem. O navegador solicita autorização antes de criar essa possibilidade.

Na disciplina, as requisições ficam assim:

Requisição feita pelo navegadorTipoPor quê
GET /livros?autor=…, sem cabeçalho própriosimplesmétodo e cabeçalhos da lista segura
POST /livros com corpo JSONpreflightContent-Type: application/json não está na lista
PATCH ou DELETE em /livros/:idpreflighto método não está na lista
qualquer rota com Authorization: Bearer …preflightAuthorization não está na lista

As três últimas linhas são o que o cliente web passa a fazer nas aulas 14 e 15. O passo 11 mostra a diferença com uma requisição de cada tipo.

A liberação na API​

A liberação fica no main.ts da API construída:

src/main.ts (API construída)
app.enableCors({
origin: process.env.CORS_ORIGIN ?? 'http://localhost:3001',
});

enableCors registra, antes de todas as rotas, um middleware com duas funções:

  • em toda resposta, acrescenta Access-Control-Allow-Origin com a origem configurada;
  • a um OPTIONS de preflight, responde sozinho 204, sem corpo, autorizando os métodos GET,HEAD,PUT,PATCH,POST,DELETE e repetindo em Access-Control-Allow-Headers os cabeçalhos que o navegador pediu. Essa resposta não passa por guard nem por controller.

Sem a chamada a enableCors, nada disso existe: o OPTIONS cai no roteamento comum, que não tem rota para esse método, e a API responde 404. É por isso que, no experimento do passo 11, toda requisição com preflight falha na pergunta.

Note o que enableCors não faz: recusar outras origens. A API responde o mesmo para qualquer uma — um curl com Origin: http://site-qualquer.test recebe 200, os dados e Access-Control-Allow-Origin: http://localhost:3001. Quem compara os dois valores e decide é o navegador. É a prova mais direta de que CORS não é controle de acesso.

A origem vem preferencialmente do .env, porque muda entre ambientes: em desenvolvimento é http://localhost:3001; numa publicação, é o endereço público do cliente web. O valor local também aparece como padrão no código para que o projeto didático funcione logo após a instalação.

Não resolva com origin: "*"

Liberar qualquer origem faz o erro desaparecer e é a sugestão mais comum em fóruns. O efeito é que qualquer site, aberto no navegador de qualquer pessoa, passa a poder ler a API por meio desse navegador. Para um catálogo público pode ser aceitável; como regra, não é — e, quando a sessão depender de cookies, o navegador recusa * de qualquer forma. Declare a origem que você conhece.

Credenciais e cabeçalhos da resposta​

Dois detalhes completam o mecanismo e voltam na aula 15.

Cookies. Numa requisição a outra origem, o fetch não envia cookies. Para enviá-los é preciso credentials: "include", e, nesse caso, a API deve responder também Access-Control-Allow-Credentials: true, com a origem exata em Access-Control-Allow-Origin — nunca *. O cabeçalho Authorization é outra coisa: quem o escreve é o código, não o navegador. Ele não depende dessa opção; só obriga o preflight.

Cabeçalhos da resposta. Mesmo com a resposta liberada, o JavaScript só lê alguns cabeçalhos dela. Na API da disciplina, resposta.headers mostra apenas content-length e content-type; o ETag, por exemplo, fica invisível. Para expor um cabeçalho próprio seria preciso Access-Control-Expose-Headers. A API não precisa disso porque o envelope da aula 6 põe total e totalDePaginas no corpo, e não em cabeçalhos — uma decisão de contrato que também simplifica o CORS.

Diagnóstico​

As mensagens do console dizem em que ponto a conferência falhou. As três que o laboratório produz:

Trecho da mensagem (Chrome)O que aconteceuCorreção
No 'Access-Control-Allow-Origin' header is present on the requested resource.requisição simples: a API respondeu, sem liberar a origemrestaurar enableCors e conferir a origem configurada
Response to preflight request doesn't pass access control checko OPTIONS não autorizou; a requisição real não saiurestaurar enableCors — e não procurar a requisição real na API
The 'Access-Control-Allow-Origin' header has a value 'http://localhost:3001' that is not equal to the supplied origin.a API liberou outra origem — por exemplo, a página aberta por 127.0.0.1abrir por localhost, ou ajustar CORS_ORIGIN

Na aba de rede das ferramentas de desenvolvedor, a requisição com preflight aparece em duas linhas: o OPTIONS e, depois, a requisição real.

O que não resolve

mode: "no-cors" no fetch faz o erro desaparecer do console, mas a resposta chega opaca: status 0, corpo ilegível. Nenhuma mudança no cliente resolve um bloqueio de CORS, porque a autorização é da API. A única alternativa do lado do cliente é não passar pelo navegador — chamar a API pelo servidor Next.js, o caminho da Parte 2. O app Flutter do Módulo 4, compilado para Android ou iOS, também não passa por CORS: não há navegador nem página com origem.


Parte 9 — A resposta atrasada e a limpeza do Effect​

Toda tecla dispara uma requisição, e nada garante que as respostas cheguem na ordem em que as requisições saíram. Digitar cl produz duas buscas: ?autor=c e ?autor=cl. Se a primeira demorar mais — servidor ocupado, rede instável —, ela chega por último e sobrescreve a mais nova. O campo mostra cl; a lista mostra os livros de Machado de Assis, que casam com c e não com cl.

É a mesma condição de corrida do Extra A: duas execuções concorrentes cujo resultado depende da ordem em que terminam, sem nenhuma thread a mais envolvida. A Figura 4 mostra a linha do tempo medida no passo 13.

Linha do tempo de 0 a 2400 milissegundos, com quatro faixas. Primeira: a tecla c dispara a busca por c, que fica esperando a API por 2000 milissegundos. Segunda: a tecla l, aos 150 milissegundos, dispara a busca por cl, que responde em poucos milissegundos. Terceira, tela sem limpeza: mostra Clarice a partir da resposta de cl e, aos 2030 milissegundos, quando a resposta de c chega por último, passa a mostrar Machado e Clarice com cl no campo, trecho em vermelho. Quarta, tela com limpeza: mostra Clarice até o fim. Uma linha tracejada vertical aos 150 milissegundos marca o momento em que a limpeza faz ignorar valer true na busca por c.
A resposta mais lenta chega por último e vence. A limpeza não cancela a requisição: marca a resposta dela para ser descartada.

A correção está nas duas linhas da Parte 7 que ficaram sem explicação. A função devolvida pelo Effect é a limpeza: o React a executa antes de rodar o Effect de novo, e quando o componente sai da tela. Cada execução do Effect tem a sua própria variável ignorar, e a limpeza da execução anterior a marca como verdadeira. Quando a resposta de c finalmente chega, o if (!ignorar) a descarta.

A requisição não é cancelada — ela chega à API e volta. Cancelar de verdade é possível com AbortController, e é o que as bibliotecas da nota da Parte 7 fazem. Para a correção da tela, descartar basta.

No modo de desenvolvimento, o Effect roda duas vezes ao montar

O modo estrito do React, ligado por padrão no Next.js, monta cada componente, desmonta e monta de novo, só em desenvolvimento. É uma forma de revelar Effect sem limpeza: se a segunda montagem produz um defeito, a primeira também produziria, em algum momento. Neste componente não há efeito visível, porque ao montar o termo está vazio e o Effect retorna antes de chamar a API. A Parte 7 mostra um Effect que busca algo já na montagem — ali a chamada dupla acontece de verdade, e o aviso daquela parte detalha a sequência passo a passo.


Parte 10 — Servidor ou navegador: como escolher​

A aula construiu os dois caminhos de propósito, e o projeto agora tem os dois funcionando lado a lado. Esta parte os compara com base no que o laboratório mede e termina com um roteiro de perguntas para decidir em cada tela.

O mesmo problema, duas soluções​

"Mostrar os livros de um autor" está resolvido duas vezes no projeto. A tabela registra quantas requisições chegam à API enquanto o leitor digita clarice, letra por letra:

SoluçãoOnde estáRequisições à API durante a digitaçãoLimite
O servidor traz a lista, e o navegador filtra em memóriao filtro do painel, em /livros (aula 12)nenhuma — a lista chegou numa única chamada, feita pelo servidor na abertura da páginaconsidera apenas o que foi recebido: tamanho=100. Um 101º livro deixaria de aparecer no filtro, sem aviso
O navegador consulta a API a cada tecla/livros/busca (esta aula)sete, uma por letraCORS, estados escritos à mão e corrida de respostas (Partes 7 a 9)

Nenhuma das duas é correta em abstrato. O filtro do painel é a melhor escolha enquanto o acervo cabe numa página; a consulta à API torna-se necessária quando deixa de caber.

Vantagens e desvantagens​

CritérioPelo servidorPelo navegador
Primeira telao HTML já chega com os dados: o HTML de /livros contém os títuloso HTML chega sem os dados — o de /livros/busca contém apenas o campo —, e os dados vêm depois, em outra ida à rede
Idas à rede até exibir o dadouma, do navegador ao Next.js; a chamada do Next.js à API costuma ocorrer dentro da mesma redepelo menos duas, a página e a API; com preflight, três
Onde a API precisa estar acessívelapenas para o servidor Next.js — ela pode permanecer numa rede interna, fora da internetna internet, ao alcance do navegador de cada leitor
Segredos e endereçosAPI_URL e qualquer credencial de serviço permanecem no servidor: o passo 14 mostra que lib/livros.ts não é enviado ao navegadortudo o que o componente utiliza vai no JavaScript público, inclusive NEXT_PUBLIC_API_URL
CORSnão se aplica: a requisição do servidor sai sem o cabeçalho Origina API precisa liberar a origem do cliente (Parte 8)
Espera e falhaloading.tsx e error.tsx, por convenção de arquivo; o erro original fica registrado no terminal de quem opera o sistematipo união, limpeza do Effect e tratamento da corrida, tudo escrito à mão; o erro fica no console do leitor, fora do alcance de quem opera o sistema
Reação à interaçãocada mudança exige uma nova requisição de página ao servidorreage a cada tecla, sem sair da tela
Carga sobre o Next.jstoda página dinâmica passa por ele e aguarda a APIa página pode ser estática; o Next.js não participa da busca
Hospedagemrotas ƒ exigem um servidor Node.js em execuçãorotas ○ podem ser servidas como arquivos estáticos
Cacheno servidor, compartilhado entre todos os leitores (aula 14)em cada navegador, com bibliotecas como SWR e TanStack Query

A tabela, lida em conjunto, justifica por que o servidor é o caminho padrão: ele entrega a primeira tela pronta, preserva o que é privado e resolve a espera e a falha com dois arquivos. O caminho pelo navegador leva vantagem em um único critério, a reação à interação, e implica custos em quase todos os demais.

Como escolher​

Para cada dado de uma tela, responda às perguntas na ordem e pare na primeira que for conclusiva:

  1. A chamada exige uma credencial de serviço, ou a API não é pública? Nesse caso, a chamada é feita pelo servidor, sem exceção: o navegador não guarda segredos nem alcança redes internas.
  2. O que a tela exibe ao abrir pode ser determinado no servidor — pela URL, pelos parâmetros ou pelos cookies? Então a chamada é feita pelo servidor. É o caso de /livros e de /livros/[id].
  3. O dado muda em resposta a uma interação contínua, sem troca de página — a cada tecla, a cada movimento? Então:
    • se o conjunto é pequeno e já está na tela, filtre no navegador, sem nova chamada, como faz o painel;
    • se o conjunto não cabe na tela, ou a consulta é custosa, chame a API pelo navegador, como faz a busca, e assuma os custos de CORS, de estados e de limpeza.

Quando nenhuma das perguntas for conclusiva, mantenha a chamada no servidor.

As telas reais combinam os dois caminhos​

A escolha é feita por dado, não por página. /livros já é uma combinação: o componente de servidor busca a lista, e o componente de cliente cuida do filtro, da sacola e do desfazer, sem chamar a API. Esse é o padrão geral — o servidor entrega o estado inicial, e o navegador assume a interação a partir dele. As bibliotecas citadas na Parte 7 seguem a mesma ideia quando aceitam dados iniciais vindos do servidor e só consultam a API quando algo muda.

E no aplicativo mobile?

O app Flutter do Módulo 4 só tem o segundo caminho: não há servidor Next.js entre ele e a API. Parte das consequências da tabela se mantém — estados de espera e de erro escritos à mão, corrida de respostas, API acessível pela internet — e parte deixa de existir: não há CORS, porque não há navegador nem página com origem.

No seu projeto

O roteiro é do padrão; as respostas são do domínio. Liste as telas do seu sistema e, para cada dado que elas exibem, responda às três perguntas acima. Registre a decisão numa tabela — tela, dado, caminho e justificativa. A maioria das linhas deve resultar em "pelo servidor"; as que não resultarem são as que exigirão CORS na sua API.


Erros comuns​

ErroSintomaCorreção
Chamar listarLivros() sem awaitTypeScript: Type 'Promise<Livro[]>' is missing the following properties…await e página async
Testar if (!livro) sobre uma Promiseo teste nunca é verdadeiroesperar antes de testar
Tratar só fetch rejeitado como falhaum 500 é lido como se fosse o livroconferir resposta.ok e lançar
Tratar 404 como falha"não foi possível carregar" para um livro que não existedevolver undefined e chamar notFound()
Página que lê a API sem declarar dependência da requisiçãoacervo congelado no build; build falha com a API fora do arawait connection(), ou revalidação (aula 14)
error.tsx sem "use client"erro de compilação pedindo componente de clientea diretiva na primeira linha
Usar API_URL em componente de clienteundefined no navegador, e a chamada vai para o endereço erradoNEXT_PUBLIC_API_URL, e um build novo
Buscar pelo navegador o que a página mostra ao abrirtela vazia antes dos dados, CORS a configurar, estados escritos à mãoler no componente de servidor (Parte 10, pergunta 2)
Chamada do navegador sem CORS na API"blocked by CORS policy" no console; o fetch rejeitaenableCors com a origem do cliente
origin: "*" para suprimir o erro de CORSqualquer site passa a poder ler a API pelo navegador do leitordeclarar a origem conhecida
Procurar na API a requisição que falhou no preflightnada chegou; o console fala em preflighta requisição real nunca saiu; conferir o OPTIONS
Abrir o cliente por 127.0.0.1:3001"…is not equal to the supplied origin" no consoleabrir por localhost:3001, a origem liberada
mode: "no-cors" para suprimir o erroresposta opaca: status 0 e corpo ilegívelliberar a origem na API
Effect de busca sem limpezaa lista às vezes não corresponde ao campoignorar e a função de limpeza
Marcar "carregando" dentro do Effectrenderização extra a cada tecla; o ESLint acusa react-hooks/set-state-in-effectmarcar no handler do evento
Tratar a chamada dupla do modo estrito como defeito/autores chamada duas vezes ao abrir a tela, só em desenvolvimentoé esperado com []; conferir com npm run build && npm run start
Esperar HTTP 404 em página com loading.tsxstatus 200, com a tela de não encontradoé o comportamento documentado; ver o aviso da Parte 5
Endereço de detalhe digitado à mão após novo seed404 para um livro que existeos id mudam a cada seed; navegue a partir da lista

Laboratório 13 — O acervo vindo da API​

O laboratório segue o mesmo domínio-guia do Módulo 2: o acervo de uma biblioteca. Os blocos No seu projeto indicam como traduzir cada passo para o domínio do seu estudo de caso.

Estado inicial esperado: o projeto ao fim do laboratório 12, rodando, com o painel do acervo sobre dados fixos; e a API construída funcionando, com o banco populado pelo seed.

Resultado ao fim deste laboratório: /livros e /livros/[id] leem a API pelo servidor, a cada requisição, com esqueleto de carregamento e tela de falha com "Tentar de novo"; /livros/busca consulta a API pelo navegador, com CORS liberado e sem a corrida de respostas.

Parte do roteiroPassos
Pôr os dois projetos no ar1 a 3
Ler pelo servidor4 e 5
Decidir quando a página é montada6 e 7
Aninhar, esperar e falhar8 a 10
Ler pelo navegador11 a 13
Fechar o ciclo14

Passo 1 — Partir do projeto da aula 12​

Copie a pasta do projeto anterior, para que ele continue existindo como estava:

cp -R biblioteca-web-aula-12 biblioteca-web-aula-13
cd biblioteca-web-aula-13
npm install

No package.json, fixe a porta do cliente nos dois scripts que sobem servidor. A 3000 passa a ser da API:

package.json (trecho)
"scripts": {
"dev": "next dev -p 3001",
"build": "next build",
"start": "next start -p 3001",
"lint": "eslint"
}

Troque também o texto do rodapé, em app/layout.tsx, para "Laboratório da aula 13", e o parágrafo de app/page.tsx que fala em acervo fixo — por exemplo, para "A partir desta aula as páginas leem o acervo da API do Módulo 2".

Passo 2 — Pôr a API no ar​

Em outro terminal, na pasta da API construída:

npm run start:dev

Abra http://localhost:3000/livros no navegador. Deve aparecer um JSON com dados contendo três livros — A Hora da Estrela, Dom Casmurro e Memórias Póstumas de Brás Cubas — e total: 3.

Os id mudam a cada seed

O seed apaga os registros e os cria de novo, e o PostgreSQL não reaproveita números de sequência. Depois de rodar npx prisma db seed outra vez, Dom Casmurro pode deixar de ser o livro 1. Ao longo do roteiro, navegue sempre a partir da lista, em vez de digitar o endereço do detalhe.

Passo 3 — Os endereços da API​

Na raiz do projeto do cliente, crie .env.example, que vai para o repositório e documenta as variáveis:

.env.example
# Endereço da API visto pelo SERVIDOR Next.js (componentes de servidor).
API_URL=http://localhost:3000

# Endereço da API visto pelo NAVEGADOR (componentes de cliente).
NEXT_PUBLIC_API_URL=http://localhost:3000

# Só para o laboratório: espera artificial, em milissegundos, antes de cada
# chamada feita pelo servidor, para o loading.tsx ficar visível.
# ATRASO_API_MS=1500

Copie-o para .env.local, que é o arquivo que o Next.js lê e que não vai para o repositório:

cp .env.example .env.local

O .gitignore gerado pelo create-next-app ignora todo arquivo que começa com .env, inclusive o exemplo. Acrescente uma linha logo abaixo de .env*, para que o exemplo volte a ser versionado:

.gitignore (trecho)
.env*
!.env.example

Passo 4 — Trocar os dados fixos por chamadas​

Em lib/livros.ts, mantenha os tipos Editora e Livro e apague tudo o que é dado: os cinco autores, as duas editoras e o vetor livros. O comentário do topo do arquivo, que fala em dados fixos, também deixa de valer; troque-o por uma linha dizendo que o módulo chama a API. Troque o tipo Autor, que passa a admitir nacionalidade nula, e acrescente o tipo do envelope logo depois de Livro:

lib/livros.ts (trecho)
export type Autor = {
id: number;
nome: string;
nacionalidade: string | null;
};
lib/livros.ts (trecho)
export type Pagina<T> = {
dados: T[];
pagina: number;
tamanho: number;
total: number;
totalDePaginas: number;
};

Depois dos tipos, o endereço da API, o instrumento de atraso e as duas funções, agora assíncronas:

lib/livros.ts (trecho)
const API_URL = process.env.API_URL ?? "http://localhost:3000";

const ATRASO_MS = Number(process.env.ATRASO_API_MS ?? 0);

function esperar(ms: number): Promise<void> {
return new Promise((resolver) => setTimeout(resolver, ms));
}

export async function listarLivros(): Promise<Livro[]> {
await esperar(ATRASO_MS);

const resposta = await fetch(`${API_URL}/livros?tamanho=100`);

if (!resposta.ok) {
throw new Error(`GET /livros respondeu ${resposta.status}`);
}

const pagina: Pagina<Livro> = await resposta.json();
return pagina.dados;
}

export async function buscarLivro(id: number): Promise<Livro | undefined> {
await esperar(ATRASO_MS);

const resposta = await fetch(`${API_URL}/livros/${id}`);

if (resposta.status === 404) {
return undefined;
}
if (!resposta.ok) {
throw new Error(`GET /livros/${id} respondeu ${resposta.status}`);
}

return resposta.json();
}

Rode npx tsc --noEmit. Devem aparecer erros nas duas páginas — em app/livros/page.tsx, Type 'Promise<Livro[]>' is missing the following properties from type 'Livro[]'; em app/livros/[id]/page.tsx, uma sequência de Property 'titulo' does not exist on type 'Promise<Livro | undefined>'. É o TypeScript apontando exatamente os dois lugares que o passo 5 corrige.

Passo 5 — Esperar nas páginas​

Em app/livros/page.tsx, a função vira async e a chamada ganha await. O resto do arquivo não muda:

app/livros/page.tsx (trecho)
export default async function Page() {
const livros = await listarLivros();

Em app/livros/[id]/page.tsx, a chamada ganha await. A página deve ter, antes dela, a verificação de formato do id publicada no passo 9 da aula 11:

app/livros/[id]/page.tsx (trecho)
const { id } = await params;

if (!/^[1-9]\d*$/.test(id) || !Number.isSafeInteger(Number(id))) {
notFound();
}

const livro = await buscarLivro(Number(id));

Se a sua página não tem a verificação, acrescente-a agora: o motivo está na Parte 3.

Suba o cliente com npm run dev e confira:

EndereçoEsperado
/livrostrês cartões e "3 de 3 títulos exibidos"
clicar em Dom Casmurroo detalhe, com editora "não informada"
clicar em A Hora da Estrelaeditora "Record"
/livros/99999"Página não encontrada"
/livros/abc"Página não encontrada"

O filtro, a sacola e o desfazer continuam funcionando sem nenhuma alteração: o painel recebe um vetor de livros por props, como antes, e não sabe de onde ele veio.

No seu projeto

Escolha a sua tela de listagem e troque os dados fixos pela chamada à rota correspondente da sua API. Se os seus componentes quebrarem por causa de um campo, o defeito está no tipo — ele descrevia os dados de exemplo, e não o contrato. Ajuste o tipo, e não o componente.

📦 Ficou para trás? O pacote aula-13-biblioteca-web-checkpoint-1.zip traz o projeto neste ponto (acervo e detalhe lidos da API pelo servidor; ainda sem connection(), sem layout aninhado e sem busca). Veja como usar em Pacotes de checkpoint.

Passo 6 — Experimento: o acervo congelado no build​

Pare o npm run dev e, com a API no ar, rode:

npm run build
npm run start

No mapa de rotas, /livros está marcada com ○ — estática. Abra http://localhost:3001/livros e confira os três títulos.

Agora altere um título pelo Swagger da API, em http://localhost:3000/docs: envie PATCH /livros/{id} com o id de Dom Casmurro e o corpo { "titulo": "Dom Casmurro (edição revista)" }.

Recarregue /livros. O título continua o antigo. Pare a API e recarregue de novo: a página continua respondendo, com os mesmos três livros. O HTML foi gerado uma vez, no build, com o que a API devolveu naquele momento.

Por fim, com a API ainda parada, apague o resultado do build anterior e rode de novo:

rm -rf .next
npm run build

Ele falha:

Error occurred prerendering page "/livros".
TypeError: fetch failed
[cause]: Error: connect ECONNREFUSED 127.0.0.1:3000

Pare o npm run start e mantenha a API parada: o passo 7 começa sem ela. Deixe o título alterado: ele serve de conferência no mesmo passo.

Passo 7 — Declarar que a página depende da requisição​

Em app/livros/page.tsx, importe connection e chame-a antes da busca:

app/livros/page.tsx
import type { Metadata } from "next";
import { connection } from "next/server";
import PainelAcervo from "@/app/components/PainelAcervo";
import { listarLivros } from "@/lib/livros";

export const metadata: Metadata = {
title: "Acervo",
};

export default async function Page() {
await connection();

const livros = await listarLivros();

return (
<>
<h1 className="text-2xl font-semibold">Acervo</h1>
<PainelAcervo livros={livros} />
</>
);
}

O <section> que envolvia a página saiu junto: quem passa a envolver as páginas do segmento é o layout do passo 8.

Com a API ainda parada, rode rm -rf .next e npm run build de novo. Desta vez ele termina, e /livros passa a ƒ: o build não chama mais a API. Suba a API e depois o cliente, com npm run start: a página mostra Dom Casmurro (edição revista). Volte o título ao original pelo Swagger, recarregue, e a mudança aparece imediatamente.

Pare o npm run start e volte ao npm run dev para os próximos passos.

📦 Ficou para trás? O pacote aula-13-biblioteca-web-checkpoint-2.zip traz o projeto neste ponto (/livros com connection(), dinâmica; ainda sem layout aninhado, sem loading.tsx/error.tsx e sem busca). Veja como usar em Pacotes de checkpoint.

Passo 8 — O layout do segmento​

Crie app/livros/layout.tsx:

app/livros/layout.tsx
import Link from "next/link";

export default function LayoutLivros({ children }: LayoutProps<"/livros">) {
return (
<section>
<nav
aria-label="Seções do acervo"
className="flex gap-4 border-b border-black/10 pb-2 text-sm dark:border-white/15"
>
<Link href="/livros" className="underline">
Todos os títulos
</Link>
<Link href="/livros/busca" className="underline">
Buscar por autor
</Link>
</nav>

<div className="mt-4">{children}</div>
</section>
);
}

Abra /livros e um detalhe: a navegação aparece nos dois. O segundo link ainda leva a "Página não encontrada" — a rota nasce no passo 11.

Passo 9 — O esqueleto de carregamento​

Crie app/livros/loading.tsx:

app/livros/loading.tsx
export default function Loading() {
return (
<div aria-busy="true" aria-live="polite">
<p className="text-sm opacity-80">Carregando o acervo…</p>
<ul className="mt-4 space-y-3">
{[1, 2, 3].map((n) => (
<li
key={n}
className="h-20 animate-pulse rounded border border-black/10 bg-black/5 dark:border-white/15 dark:bg-white/5"
/>
))}
</ul>
</div>
);
}

Com a API na mesma máquina, a resposta chega em milissegundos e o esqueleto quase não aparece. Em .env.local, descomente ATRASO_API_MS=1500 e reinicie o npm run dev — variáveis de ambiente só são lidas quando o servidor sobe.

Navegue de um detalhe para "Todos os títulos". Por cerca de um segundo e meio, a navegação do acervo continua no lugar e só a área da página mostra os três cartões pulsando, como registra a Figura 5 — o comportamento representado na Figura 2.

Captura do cliente web em tema claro. No alto, o cabeçalho com Biblioteca e Acervo; abaixo, a navegação do acervo com os links Todos os títulos e Buscar por autor, sobre uma linha divisória. Na área da página, o texto Carregando o acervo e três retângulos cinza-claros vazios, do tamanho de cartões de livro.
O esqueleto no lugar da página, com o atraso de laboratório ligado. O cabeçalho e a navegação do acervo já estão na tela: estão fora da fronteira de Suspense.
Em máquinas gerenciadas (laboratório, notebook corporativo), o esqueleto pode não aparecer

Se a tela pular direto do conteúdo anterior para o acervo completo — sem o esqueleto pulsando no meio —, mesmo depois de reiniciar o npm run dev com ATRASO_API_MS descomentado, o defeito costuma ser do ambiente, não do código. Antivírus corporativo com módulo de "proteção de rede" e proxy HTTP de todo o sistema (ambos comuns em laboratórios de universidade, mesmo sem máquina virtual no meio) interceptam o tráfego até em localhost e remontam a resposta inteira antes de liberá-la ao navegador — o que anula o streaming que faz o esqueleto aparecer primeiro.

Para confirmar, abra a aba Network das ferramentas de desenvolvedor, clique na requisição de /livros e compare, na aba Timing, o tempo de TTFB (Time to First Byte, chamado também de "Waiting for server response") com o tempo total. Se os dois forem iguais — por volta de 1,5 segundo —, a resposta já chegou inteira de uma vez a algo entre o Next.js e o navegador: o servidor está enviando o esqueleto em milissegundos (como a Parte 5 descreve), só que ele não teve como passar primeiro. Nessas máquinas, raramente dá para desativar o antivírus ou o proxy; trate a medição do Network tab como a evidência do conceito e siga para o passo 10.

Agora confira o status de um livro inexistente, em outro terminal:

curl -s -o /dev/null -w "%{http_code}\n" http://localhost:3001/livros/99999

A resposta é 200, embora a tela seja "Página não encontrada". O aviso da Parte 5 explica por quê.

Comente de novo ATRASO_API_MS e reinicie o npm run dev.

Passo 10 — A tela de falha​

Crie app/livros/error.tsx:

app/livros/error.tsx
"use client";

export default function Erro({
error,
retry,
}: {
error: Error & { digest?: string };
retry: () => void;
}) {
return (
<div role="alert" className="rounded border border-red-600/40 p-4">
<p className="font-semibold">Não foi possível carregar o acervo.</p>
<p className="mt-1 text-sm opacity-80">
O serviço da biblioteca não respondeu como esperado. Confira se a API
está no ar e tente de novo.
</p>

{error.digest && (
<p className="mt-3 font-mono text-xs opacity-70">
código {error.digest}
</p>
)}

<button
type="button"
onClick={() => retry()}
className="mt-4 rounded border px-3 py-1 text-sm"
>
Tentar de novo
</button>
</div>
);
}

Pare a API e recarregue /livros. A navegação do acervo continua no alto; no lugar da página aparece a mensagem, com um código, como na Figura 6. No canto inferior da tela, o indicador do Next.js passa a mostrar "1 Issue"; clicando nele aparece o erro original, fetch failed, com a linha de lib/livros.ts que o lançou. Esse indicador só existe em desenvolvimento.

Captura do cliente web em tema claro, com a API desligada. O cabeçalho e a navegação do acervo aparecem normalmente. Na área da página, uma caixa com borda vermelha contém o título Não foi possível carregar o acervo, uma frase pedindo para conferir se a API está no ar, a linha código 3662023945 em fonte monoespaçada e o botão Tentar de novo.
A tela de falha ocupa só o lugar da página. O código é o digest do erro, o mesmo número que o terminal do servidor imprime ao lado da causa verdadeira.

Suba a API de novo e clique em Tentar de novo: o acervo aparece, sem recarregar a página.

Para ver a diferença de produção, pare o npm run dev — o npm run start usa a mesma porta 3001 — e rode npm run build e npm run start; o build já não depende da API. Pare a API e recarregue: a mensagem é a mesma, e o código na tela é o mesmo número que aparece no terminal do npm run start, na linha digest: logo abaixo de TypeError: fetch failed. É por esse número que se chega do relato de um usuário ao erro verdadeiro.

Antes do próximo passo, pare o npm run start, suba a API de novo e volte ao npm run dev.

No seu projeto

O texto da tela de falha é do domínio; a estrutura é do padrão. Escreva a mensagem na linguagem de quem usa o seu sistema, sem nome de tecnologia, e mantenha o retry e o digest.

📦 Ficou para trás? O pacote aula-13-biblioteca-web-checkpoint-3.zip traz o projeto neste ponto (layout aninhado, loading.tsx e error.tsx prontos; ainda sem a busca pelo navegador). Veja como usar em Pacotes de checkpoint.

Passo 11 — A busca pelo navegador​

Crie app/components/BuscaPorAutor.tsx, o único componente novo da aula:

app/components/BuscaPorAutor.tsx
"use client";

import { useEffect, useState } from "react";
import LivroCard from "@/app/components/LivroCard";
import type { Livro, Pagina } from "@/lib/livros";

const API_PUBLICA = process.env.NEXT_PUBLIC_API_URL ?? "http://localhost:3000";

type Busca =
| { situacao: "carregando" }
| { situacao: "erro" }
| { situacao: "pronta"; livros: Livro[] };

export default function BuscaPorAutor() {
const [termo, setTermo] = useState("");
const [busca, setBusca] = useState<Busca>({ situacao: "carregando" });

const termoLimpo = termo.trim();

useEffect(() => {
if (termoLimpo === "") {
return;
}

let ignorar = false;

const url = `${API_PUBLICA}/livros?autor=${encodeURIComponent(termoLimpo)}`;

fetch(url)
.then((resposta) => {
if (!resposta.ok) {
throw new Error(`GET /livros respondeu ${resposta.status}`);
}
return resposta.json() as Promise<Pagina<Livro>>;
})
.then((pagina) => {
if (!ignorar) {
setBusca({ situacao: "pronta", livros: pagina.dados });
}
})
.catch(() => {
if (!ignorar) {
setBusca({ situacao: "erro" });
}
});

return () => {
ignorar = true;
};
}, [termoLimpo]);

function mudarTermo(novoTermo: string) {
setTermo(novoTermo);
if (novoTermo.trim() !== termoLimpo) {
setBusca({ situacao: "carregando" });
}
}

return (
<div className="mt-4 space-y-4">
<label className="block text-sm">
Autor
<input
type="search"
value={termo}
onChange={(evento) => mudarTermo(evento.target.value)}
placeholder="Digite parte do nome"
className="mt-1 block w-full rounded border border-black/20 px-3 py-2 dark:border-white/25"
/>
</label>

{termoLimpo === "" ? (
<p className="text-sm opacity-80">
Digite parte do nome de um autor para consultar o acervo.
</p>
) : busca.situacao === "carregando" ? (
<p className="text-sm opacity-80" aria-live="polite">
Buscando…
</p>
) : busca.situacao === "erro" ? (
<p role="alert" className="text-sm">
Não foi possível consultar a API.
</p>
) : busca.livros.length === 0 ? (
<p className="text-sm">Nenhum autor corresponde à busca.</p>
) : (
<ul className="space-y-3">
{busca.livros.map((livro) => (
<li key={livro.id}>
<LivroCard livro={livro} />
</li>
))}
</ul>
)}
</div>
);
}

E a página que o usa, app/livros/busca/page.tsx:

app/livros/busca/page.tsx
import type { Metadata } from "next";
import BuscaPorAutor from "@/app/components/BuscaPorAutor";

export const metadata: Metadata = {
title: "Buscar por autor",
};

export default function Page() {
return (
<>
<h1 className="text-2xl font-semibold">Buscar por autor</h1>
<BuscaPorAutor />
</>
);
}

A pasta busca fica ao lado de [id], e as duas casam com /livros/busca. O App Router resolve a favor do segmento fixo: o dinâmico só é tentado quando nenhum fixo corresponde.

Para observar primeiro o bloqueio, comente temporariamente a chamada a app.enableCors(...) no src/main.ts da API e reinicie-a. Abra /livros/busca, com o console do navegador aberto, e digite clarice. A tela mostra "Não foi possível consultar a API.", e o console mostra o bloqueio de CORS da Parte 8.

Confira que a API respondeu, mesmo assim:

curl -i -H "Origin: http://localhost:3001" "http://localhost:3000/livros?autor=clarice"

A resposta é 200 OK, com A Hora da Estrela no corpo — e nenhum cabeçalho Access-Control-Allow-Origin. A API respondeu; foi o navegador que reteve a resposta.

Agora uma requisição com preflight. Abra a aba de rede das ferramentas de desenvolvedor e, no console da mesma página, envie um POST com corpo JSON:

console do navegador, em localhost:3001
fetch("http://localhost:3000/livros", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: "{}",
});

A aba de rede mostra um OPTIONS /livros respondido com 404, e o console acusa Response to preflight request doesn't pass access control check. O POST não chegou à API: sem enableCors, ela não tem rota para o método OPTIONS, a consulta prévia falhou e o navegador não enviou a requisição real. O experimento não cadastra nenhum registro; quando a consulta prévia for autorizada, o corpo vazio ainda será rejeitado pela validação da API.

A mesma pergunta pode ser feita à mão:

curl -i -X OPTIONS \
-H "Origin: http://localhost:3001" \
-H "Access-Control-Request-Method: POST" \
-H "Access-Control-Request-Headers: content-type" \
http://localhost:3000/livros

A resposta é 404 Not Found, com Cannot OPTIONS /livros no corpo.

Passo 12 — Liberar a origem na API​

Restaure a chamada a app.enableCors(...) que você comentou no passo anterior. O .env copiado do projeto já traz a origem do cliente:

.env (API construída)
CORS_ORIGIN="http://localhost:3001"

O src/main.ts lê essa variável logo depois de criar a aplicação:

src/main.ts (API construída, trecho)
app.enableCors({
origin: process.env.CORS_ORIGIN ?? 'http://localhost:3001',
});

A leitura fica depois do create() porque é o ConfigModule, carregado ali, que copia o .env para process.env.

Reinicie a API. O mesmo curl do passo 11 passa a mostrar Access-Control-Allow-Origin: http://localhost:3001. No navegador, digite de novo:

TermoEsperado
clariceum cartão, A Hora da Estrela
machadodois cartões
zzz"Nenhum autor corresponde à busca."
campo vazio"Digite parte do nome de um autor…"

Os erros de CORS desaparecem do console.

Repita o fetch do POST no console. Agora a aba de rede mostra o OPTIONS com 204 e, logo depois, o POST com 400: a pergunta foi autorizada, a requisição real saiu, e o corpo vazio foi recusado pelos pipes de validação. O curl -X OPTIONS do passo 11 mostra a autorização:

resposta do OPTIONS (trecho)
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: http://localhost:3001
Vary: Origin, Access-Control-Request-Headers
Access-Control-Allow-Methods: GET,HEAD,PUT,PATCH,POST,DELETE
Access-Control-Allow-Headers: content-type

Por fim, confira que a API não recusa nenhuma origem, trocando-a:

curl -i -H "Origin: http://site-qualquer.test" "http://localhost:3000/livros?autor=clarice"

A resposta é 200 OK, com os dados e com Access-Control-Allow-Origin: http://localhost:3001. A API declara qual origem autoriza; quem compara e bloqueia é o navegador.

Passo 13 — Experimento: a resposta atrasada​

A corrida da Parte 9 é rara com a API na mesma máquina, porque as respostas chegam na ordem. O experimento força a primeira letra a demorar. Em BuscaPorAutor.tsx, troque o começo da cadeia:

app/components/BuscaPorAutor.tsx — alteração temporária
// EXPERIMENTO: a primeira letra demora dois segundos para responder.
const atraso = termoLimpo.length === 1 ? 2000 : 0;

new Promise((resolver) => setTimeout(resolver, atraso))
.then(() => fetch(url))
.then((resposta) => {

E remova a limpeza — as três linhas do return () => { ignorar = true; };.

Digite cl rapidamente. A lista mostra A Hora da Estrela, como deveria. Dois segundos depois, sem nova tecla, ela passa a mostrar também os dois livros de Machado de Assis — com cl no campo. Nenhum erro no console, nenhum no terminal, nenhum no TypeScript. O único sinal está no ESLint, que aponta 'ignorar' is never reassigned. Use 'const' instead: o aviso indica que nada mais altera a variável que deveria descartar a resposta.

Devolva a limpeza e repita: a lista fica em A Hora da Estrela. A resposta de c continua chegando depois de dois segundos — e é descartada.

Desfaça a alteração do atraso ao terminar.

📦 Ficou para trás? O pacote aula-13-biblioteca-web-checkpoint-4.zip traz o laboratório completo desta aula (busca por autor pelo navegador, com CORS e a limpeza do Effect). Veja como usar em Pacotes de checkpoint.

Passo 14 — Fechar o ciclo​

Com a API no ar:

npm run lint
npm run build

O mapa de rotas deve mostrar:

┌ ○ /
├ ○ /_not-found
├ ƒ /livros
├ ƒ /livros/[id]
└ ○ /livros/busca

/livros/busca é estática, e não há contradição: a página não busca nada no servidor. O HTML dela é o campo vazio; a busca acontece inteira no navegador.

Por fim, confira o que foi para o navegador. Gere um build com um endereço público diferente, só para poder procurá-lo, e procure dois trechos nos pacotes de cliente:

NEXT_PUBLIC_API_URL=http://api.exemplo.test npm run build
grep -rl "api.exemplo.test" .next/static/chunks # 1 arquivo: o da busca
grep -rl "tamanho=100" .next/static/chunks # nenhum: lib/livros.ts não viajou

O endereço público foi escrito dentro do JavaScript da busca, no momento do build. O módulo de acesso das páginas, com API_URL e o tamanho=100, não foi enviado ao navegador: só componentes de servidor o importam, e o componente de cliente importa dele apenas tipos, que desaparecem na compilação. Rode npm run build outra vez, sem a variável, antes de subir o projeto.

Critérios de conclusão​

O laboratório está completo quando:

  • a API roda em localhost:3000 e o cliente em localhost:3001, ao mesmo tempo;
  • /livros mostra os três livros do seed, e o filtro, a sacola e o desfazer da aula 12 continuam funcionando;
  • o detalhe de A Hora da Estrela mostra a editora Record, e o de Dom Casmurro, "não informada";
  • /livros/99999 e /livros/abc mostram "Página não encontrada";
  • você reproduziu o passo 6 — título alterado que não aparece, página que responde com a API parada, build que falha — e explicou cada um;
  • com connection(), /livros aparece como ƒ e reflete imediatamente uma alteração feita na API;
  • com o atraso ligado, a navegação do acervo fica na tela enquanto o esqueleto ocupa a área da página;
  • com a API parada, /livros mostra a tela de falha, e "Tentar de novo" recupera o acervo quando a API volta;
  • em produção, o código mostrado na tela é o mesmo digest do terminal;
  • a busca por autor falhou por CORS antes do passo 12 e funciona depois dele;
  • na aba de rede, o OPTIONS que antecede o POST respondeu 404 antes do passo 12 e 204 depois, e você explicou por que o POST não chegou à API na primeira vez;
  • você reproduziu a corrida do passo 13, sem a limpeza, e a viu desaparecer com ela;
  • npm run lint e npm run build terminam sem apontamentos, com o mapa de rotas do passo 14;
  • a sua tela de listagem lê a sua API, no seu domínio.

Fechamento​

O acervo agora vem da API, apoiado em quatro decisões que valem para qualquer tela que leia um serviço — e nenhuma delas é sobre sintaxe.

A primeira foi de que lado a chamada acontece. O que a página precisa mostrar ao abrir é buscado no servidor; o que depende de interação é buscado no navegador. O primeiro caminho é o padrão, e o segundo implica os custos vistos nas Partes 7 a 9: estados escritos à mão, CORS e corrida. A Parte 10 reúne os critérios dessa escolha.

A segunda foi o que mostrar em cada desfecho. Dados, ausência e falha são três respostas diferentes, e confundir as duas últimas é o erro que mais aparece para quem usa o sistema.

A terceira foi quando a página é montada. Sem declarar a dependência da requisição, o framework faz o que a aula 11 ensinou e gera a página uma vez — o que, com dados que mudam, significa exibir dados desatualizados.

A quarta foi quem autoriza o navegador a ler a API. A resposta é a própria API, e a conferência é do navegador — por isso a chamada pelo servidor nunca dependeu dela.

A aula 14 faz o caminho inverso: o cliente passa a enviar dados à API, com formulários, e a pergunta sobre quando a página é montada volta como pergunta sobre cache — quando uma tela que foi gerada precisa deixar de valer.


Exercícios (checkpoints)​

  1. Decida de que lado cada chamada deve acontecer, justificando com as perguntas da Parte 10: (a) a lista de empréstimos de um leitor, na página do leitor; (b) sugestões de título enquanto o bibliotecário digita; (c) o total de exemplares disponíveis de um livro, na página de detalhe; (d) a verificação, ao sair de um campo, de que um ISBN ainda não está cadastrado.

  2. Explique por que um fetch que responde 500 não cai no catch de uma chamada, e escreva as duas linhas que fazem a função de acesso tratá-lo como falha.

  3. Preveja o que o leitor vê e qual status HTTP recebe, na versão final do laboratório, em cada caso: (a) /livros/3, com o livro existindo; (b) /livros/99999; (c) /livros/3, com a API parada. Indique qual arquivo especial responde por cada tela.

  4. Diagnostique: um colega publicou o cliente web e relata que um livro cadastrado ontem não aparece na listagem, embora apareça em GET /livros. O detalhe dele abre normalmente. Aponte a causa provável, explique por que o detalhe não sofre do mesmo problema e proponha duas correções diferentes.

  5. Compare connection() e next: { revalidate: 60 } para a página do acervo: o que cada um custa à API e ao leitor, e em que situação você escolheria cada um.

  6. Explique por que a mesma requisição a GET /livros?autor=clarice funciona no curl, funciona no componente de servidor e falha no componente de cliente, e identifique quem aplica a regra que a faz falhar.

  7. Diagnostique: numa tela de busca, o campo mostra rosa, mas a lista mostra livros de autores que contêm ro e não rosa. O defeito acontece poucas vezes, e só na rede da instituição. Aponte a causa, explique por que ele não aparece em desenvolvimento e indique a correção.

  8. Refatore BuscaPorAutor para cancelar a requisição anterior com AbortController em vez de descartar a resposta com ignorar, e explique o que muda para a API e o que muda para a tela.

  9. Implemente um loading.tsx próprio para /livros/[id], com o esqueleto de um detalhe em vez de uma lista, e explique, com a Figura 2, por que ele passa a valer só para o detalhe.

  10. Classifique como simples ou com preflight, justificando pelas condições da Parte 8, estas requisições feitas pelo navegador a http://localhost:3000: (a) GET /livros?autor=rosa; (b) DELETE /livros/7; (c) GET /auth/perfil com Authorization: Bearer …; (d) POST /livros com Content-Type: application/json. Para as que têm preflight, escreva os cabeçalhos Access-Control-Request-* que o navegador envia e diga o que chega à API quando enableCors está desativado.


Referências​

Principais​

Aprofundamento​