Pular para o conteúdo principal

Aula 11: Introdução ao Next.js

O Módulo 2 terminou com um serviço completo: recursos modelados, dados persistidos, erros padronizados e acesso controlado por token nas operações protegidas. A interface para consultar o acervo será uma aplicação separada. Esta aula abre o Módulo 3 e começa a escrever o primeiro dos dois clientes que consomem esse serviço.

Como na aula 5, o foco aqui é a estrutura da aplicação. Ao fim da aula você terá três rotas mostrando dados fixos, o que é pouco. O que importa é onde cada arquivo mora, o que o nome dele significa e em qual máquina o código que você escreveu vai rodar — três decisões que o framework toma por você e que ficam caras de desfazer depois.

O que vem depoisOnde
Componentes, props, estado, eventos e composiçãoAula 12 — Fundamentos de React
Consumo da API construída no Módulo 2, layouts aninhados, estados de carregamento e erro e estratégias de renderizaçãoAula 13 — Dados da API
Formulários, envio de dados, 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:

  • Justificar o uso de um framework em vez do React sozinho, listando o que precisaria ser montado à mão sem ele.
  • Explicar a diferença entre componente de servidor e componente de cliente, e decidir qual usar diante de um requisito.
  • Descrever como o App Router transforma pastas e nomes de arquivo em URLs.
  • Identificar o papel de cada arquivo especial (page, layout, not-found e os demais).
  • Implementar rotas estáticas e dinâmicas, um layout compartilhado e componentes próprios.
  • Diagnosticar dois erros comuns de quem começa: evento em componente de servidor e params acessado sem await.
  • Ler a saída de next dev e de next build, distinguindo rota estática de rota dinâmica.

Ambiente sugerido​

Pré-requisitos: TypeScript (aula 3), contratos HTTP e o modelo da biblioteca das aulas 6 a 10. Revise funções, objetos, módulos e async/await; o Extra A — Assincronismo oferece apoio a essa última parte.

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

FerramentaVersãoObservação
Node.js22 LTS ou superiornode --version; o Next.js 16 exige 20.9 no mínimo
npma que acompanha o Nodenpm --version
Next.js16.xexemplos verificados com 16.3.4; o comando com @latest instala a versão estável atual
React19.2.xinstalado pelo create-next-app; confira package.json e preserve package-lock.json
TypeScript5.xmínimo 5.1
NavegadorChrome, Edge, Firefox ou Safari recenteso Next.js 16 tem como alvo Chrome 111+, Firefox 111+ e Safari 16.4+
Material de terceiros costuma ser da versão 15 ou anterior

Confira a versão do tutorial. params e searchParams tornaram-se Promises no Next.js 15, ainda com compatibilidade temporária de acesso síncrono; no 16 essa compatibilidade foi removida. Nesta aula usamos await. Além disso, o Turbopack virou o bundler padrão, no lugar do webpack; e o comando next lint deixou de existir, junto com a verificação automática de estilo durante o next build. Código copiado de material antigo falha por esses motivos antes de falhar por qualquer outro. Confira a versão adotada na turma antes de começar.


Conteúdo​

Vocabulário para ler os exemplos​

Um componente é uma função que descreve parte da interface. As props são os dados que ele recebe. JSX é a sintaxe de marcação dentro do JavaScript; arquivos TypeScript que a usam têm extensão .tsx. As chaves em <h1>{livro.titulo}</h1> inserem uma expressão na marcação. Um bundler compila e agrupa módulos para execução. A aula 12 aprofunda componentes, props e eventos; aqui basta reconhecer essas peças.

Parte 1 — Por que um framework, e não só o React​

O React é uma biblioteca de interface: descreve elementos a partir de dados e atualiza a tela quando o estado muda. Assim como o NestJS organiza o desenvolvimento sobre o Node.js na aula 5, o Next.js oferece convenções e ferramentas para construir uma aplicação sobre o React.

O que o React deliberadamente não resolve:

PerguntaQuem responde, sem framework
Que tela mostrar para a URL /livros/3?você, escrevendo ou escolhendo um roteador
Como o navegador recebe HTML já pronto, em vez de uma página em branco que se preenche depois?você, montando renderização no servidor
Como transformar TypeScript e JSX em algo que o navegador entenda?você, configurando um bundler
De onde vêm os dados, e quando são buscados?você, escolhendo e ligando uma biblioteca de dados
Como as imagens são redimensionadas e as fontes carregadas sem travar a tela?você, otimização por otimização

Escolher e integrar essas ferramentas exige trabalho de configuração e manutenção. Um framework oferece uma combinação pronta e documentada.

Um framework opinativo adota convenções para essas decisões. O Next.js é a escolha desta disciplina: integra roteamento, renderização e ferramentas de desenvolvimento. Isso reduz a configuração inicial, mas exige aprender suas convenções.

React puro ainda tem lugar

Uma interface que roda inteiramente dentro de outra aplicação — um painel embutido, um editor, um componente distribuído como biblioteca — não precisa de rotas nem de servidor, e aí o React com um bundler simples é mais adequado. O framework pode ajudar quando a aplicação combina rotas, renderização no servidor e busca de dados. Ter URLs, por si só, não obriga a usar um framework.


Parte 2 — O que o Next.js acrescenta​

O que acrescentaEm uma frase
Roteamento por sistema de arquivosa estrutura de pastas de app/ é o mapa de URLs; não existe arquivo de rotas
Renderização no servidorpor padrão os componentes rodam no servidor e o navegador recebe HTML pronto
Fronteira explícita servidor/cliente"use client" define a entrada do código de componentes que também executa no navegador
Compilação e empacotamentoTurbopack, já configurado, com recarga rápida em desenvolvimento
Otimizações de entregaimagens, fontes e scripts tratados por componentes próprios
Camada de dados e cachebusca de dados dentro do próprio componente, com controle de cache

As três primeiras linhas são o assunto desta aula. A quarta aparece no laboratório. As duas últimas ficam para as aulas 13 e 14 — sem elas o projeto já roda, e introduzi-las agora só encheria a primeira aula de conceitos sem uso imediato.


Parte 3 — Onde o código executa​

Esta é a diferença mais importante entre o Next.js e o React que você provavelmente já viu, e a origem dos erros mais confusos de quem começa.

No App Router, páginas e layouts são componentes de servidor por padrão. Os componentes importados por eles permanecem no servidor enquanto não entrarem em uma fronteira de cliente. Neste laboratório, executam em Node.js, durante o build ou ao atender uma requisição.

O código de um componente de servidor não é enviado ao navegador. Porém, dados colocados no HTML ou passados como props podem chegar ao cliente: não renderize segredos nem os passe como props. Neste curso, mesmo o código Next.js que executa no servidor consumirá a API NestJS; o acesso direto ao banco continua sendo responsabilidade dessa API.

A diretiva "use client", antes dos imports, define uma entrada para o código que também executa no navegador. O payload RSC é uma representação do resultado dos componentes de servidor e das referências aos componentes de cliente; RSC significa React Server Components.

Dois painéis representam os componentes de servidor e o navegador. Layout, página de detalhe e LivroCard ficam à esquerda; BotaoCopiarIsbn fica à direita. Setas indicam o resultado renderizado e props serializáveis, como o ISBN. As notas distinguem o JavaScript de cliente enviado do código de servidor e dos handlers comuns que não são enviados como props; segredos não devem ser incluídos no resultado.
O servidor envia o resultado da renderização e dados serializáveis; a interatividade usa o JavaScript dos componentes de cliente.

A Figura 1 responde à pergunta prática: quando marcar um componente com "use client"?

Precisa deOnde o componente tem de estar
Estado, eventos (onClick, onChange)cliente
APIs do navegador (window, localStorage, navigator)cliente
Estado e efeitos, com hooks como useState e useEffect (aula 12)cliente
Acessar um serviço interno com credenciais privadasservidor; neste curso, por meio da API NestJS
Usar chave ou segredo que não deve ser públicoservidor, sem expor o valor na resposta
Só formatar e exibir dados que recebeupode ficar no servidor ou compor uma interface de cliente

A regra prática que evita a maior parte dos problemas: marque o menor componente possível. A diretiva não vale só para o arquivo em que aparece — seus imports de execução e dependências transitivas entram no grafo de cliente. Imports usados apenas como tipos são removidos na compilação. Um componente de servidor passado por children não vira componente de cliente por estar visualmente dentro de um deles. A fronteira segue os imports, não toda a árvore visual. O layout deste laboratório também exporta metadata, permitido apenas em componentes de servidor.

Uma função comum, como um handler de clique, não pode ser passada do servidor ao cliente. Server Functions são uma exceção: funções marcadas com "use server" podem ser referenciadas pelo cliente para execução remota; seu corpo continua no servidor. Não são necessárias neste laboratório.

A diretiva não escolhe um único ambiente

Um componente de cliente também participa da renderização no servidor durante o carregamento inicial da página — é assim que o navegador recebe uma prévia em HTML em vez de uma tela em branco. A diretiva "use client" define uma fronteira: as exportações daquele arquivo e seus imports de execução passam a fazer parte do grafo de JavaScript enviado ao navegador.

A hidratação associa esse JavaScript ao HTML inicial e ativa a interatividade. Por isso, APIs exclusivas do navegador, como window e navigator, devem ser acessadas em eventos ou efeitos, não durante a renderização inicial. Nesse contexto, “componente de cliente” significa que ele também executa no cliente, e não que execute somente nele.


Parte 4 — App Router: a pasta é a URL​

O Next.js tem dois roteadores. O App Router, na pasta app/, é o atual e o único tratado nesta disciplina. O Pages Router, na pasta pages/, é o anterior, continua funcionando e domina o material antigo da internet — se um exemplo fala em getServerSideProps ou _app.js, ele é do outro roteador e não encaixa aqui.

Não existe arquivo onde as rotas são registradas. A árvore de pastas dentro de app/ é o mapa de endereços, e o nome do arquivo define o papel de cada peça.

Árvore de app à esquerda, com layout.tsx, page.tsx, not-found.tsx, components/LivroCard.tsx, livros/page.tsx e livros/[id]/page.tsx. Setas ligam as três páginas aos endereços /, /livros e /livros/1 ou outros IDs. Notas distinguem arquivos auxiliares e telas públicas; route.ts publica um endpoint HTTP, e grupos de rotas não acrescentam segmentos à URL.
A mesma pasta pode conter arquivos que viram URL e arquivos que não viram — quem decide é o nome. Repare que components/ está dentro de app/ sem publicar nada.

Como mostra a Figura 2, três mecanismos bastam para ler qualquer projeto:

Pasta define caminho. app/livros/page.tsx atende /livros. Renomear a pasta muda o endereço nesse caso simples. Há convenções adicionais, como grupos de rotas (grupo), que organizam arquivos sem acrescentar um segmento à URL; elas ficam fora do escopo do módulo.

Nome define papel. Só page.tsx publica uma tela — e route.ts, um endpoint HTTP, que é assunto da aula 14. Uma pasta sem nenhum dos dois é apenas organização: é por isso que app/components/ pode morar dentro de app/ sem virar rota.

Colchetes definem segmento dinâmico. app/livros/[id]/page.tsx atende /livros/1, /livros/2 e qualquer outro valor naquela posição. O valor chega ao componente pela prop params:

app/livros/[id]/page.tsx
export default async function Page({ params }: PageProps<"/livros/[id]">) {
const { id } = await params;
// ...
}

Duas coisas nesse trecho merecem atenção, porque as duas são fonte de erro:

  • params é uma Promise, e por isso a função é async e o valor sai de um await. A mudança começou no Next.js 15; no 16, o acesso síncrono deixou de ser suportado.
  • id é texto, sempre, mesmo quando a URL parece um número. Comparar "3" com 3 não casa; converta antes.

PageProps é um tipo gerado pelo próprio Next.js a partir da estrutura de pastas, junto com LayoutProps. Não precisa ser importado, e é gerado quando você roda next dev, next build ou next typegen. Em um projeto recém-clonado, rode npx next typegen se o editor ainda não reconhecer esses tipos.

Layouts​

Um layout.tsx envolve tudo o que está no seu segmento e abaixo dele, e recebe o conteúdo pela prop children. O layout na raiz de app/ é obrigatório e é o único lugar onde ficam as marcas <html> e <body>.

A diferença que importa: ao navegar entre /livros e /livros/3, o layout é preservado na navegação pelo cliente, incluindo o estado dos componentes interativos que ele contém. Isso não garante manter qualquer foco ou posição de rolagem: a navegação também gerencia esses aspectos. Layouts aninhados são o assunto da aula 13.


Parte 5 — Os arquivos especiais​

Dentro de app/, um punhado de nomes é reservado. Vale conhecer a lista dos nomes mais frequentes, mesmo usando apenas parte deles nesta aula: quando um comportamento parecer mágico, quase sempre é um destes arquivos.

ArquivoO que fazOnde a disciplina trata
page.tsxpublica uma tela em um endereçoaula 11
layout.tsxenvolve o segmento e o que estiver abaixo; preserva estado na navegaçãoaulas 11 e 13
not-found.tsxresposta para rota inexistente ou para uma chamada a notFound()aula 11
loading.tsxo que mostrar enquanto o conteúdo do segmento carregaaula 13
error.tsxo que mostrar quando o segmento lança uma exceçãoaula 13
template.tsxcomo o layout, mas reconstruído a cada navegaçãofora do escopo
route.tsexpõe um endpoint HTTP em vez de uma telaaula 14
default.tsxconteúdo padrão de rotas paralelasfora do escopo
proxy.tsintercepta requisições antes de chegarem à rota (fora de app/)aula 15
Se você já viu middleware.ts

Era o nome anterior de proxy.ts. O arquivo antigo ainda funciona no Next.js 16, mas está descontinuado. A migração também exige conferir o runtime: proxy.ts usa Node.js. Ele pode redirecionar o usuário, mas não substitui a autorização na API, implementada na aula 10. A aula 15 retoma esse assunto.


Para navegar entre páginas, use o componente <Link>:

import Link from "next/link";

<Link href={`/livros/${livro.id}`}>{livro.titulo}</Link>;

Ele produz um <a> no HTML final — a diferença está no que acontece ao clicar. Um <a> comum para outra página faz uma navegação de documento completo: o layout é reconstruído e o estado React em memória é reiniciado. O <Link> permite a transição pelo cliente, preservando os layouts compartilhados.

Em produção, ele também pode fazer prefetch: buscar conteúdo antes do clique quando o link aparece na tela. Rotas estáticas podem ser antecipadas por completo; rotas dinâmicas podem não ser antecipadas ou receber antecipação parcial com loading.tsx. O prefetch automático não ocorre em desenvolvimento. Ele reduz a espera, mas não garante navegação instantânea.

Vale a pena saber quando não usar: para endereços fora da aplicação, um <a> normal é o certo. Não há nada a antecipar em um site que não é seu.


Parte 7 — O ciclo de desenvolvimento​

Quatro comandos fazem parte do ciclo básico:

npm run dev # desenvolvimento, com recarga a cada gravação
npm run lint # verifica o código com ESLint
npm run build # compila a versão de produção e verifica os tipos
npm run start # sobe o que o build produziu

O npm run dev imprime algo assim:

▲ Next.js 16.3.4 (Turbopack)
- Local: http://localhost:3000
- Network: http://192.168.0.10:3000
✓ Ready in 405ms

Na versão de referência, o registro de uma requisição pode separar o tempo entre framework e aplicação. Os nomes e os tempos variam por versão:

GET /livros 200 in 329ms (next.js: 262ms, application-code: 67ms)

A separação ajuda a investigar lentidão: application-code inclui o tempo atribuído à execução da aplicação, inclusive esperas; next.js corresponde ao trabalho do framework. A primeira compilação em desenvolvimento pode custar mais. Esse registro orienta a investigação, mas não substitui uma medição em produção.

Já o npm run build termina com um mapa das rotas:

Route (app)
┌ ○ /
├ ○ /_not-found
├ ○ /livros
└ ƒ /livros/[id]

○ (Static) prerendered as static content
ƒ (Dynamic) server-rendered on demand

Vale ler com atenção. As rotas marcadas com ○ foram geradas durante o build e podem ser servidas sem renderizar novamente a página no servidor; o JavaScript interativo ainda executa no navegador. A marcada com ƒ é renderizada no servidor sob demanda. Neste laboratório, os IDs não foram enumerados para pré-renderização com generateStaticParams. Um segmento [id] não obriga, por si só, a renderizar a cada requisição: a aula 13 mostrará como pré-renderizar caminhos conhecidos. Esse mapa supõe a configuração padrão, sem ativar cacheComponents.

A porta 3000 é a mesma da API

O serviço do Módulo 2 também sobe em http://localhost:3000. Nesta aula isso não incomoda, porque o cliente ainda não fala com a API — mas se as duas coisas estiverem no ar, o Next.js avisa (Port 3000 is in use...) e passa sozinho para a 3001. A partir da aula 13, quando os dois precisam rodar juntos, vale fixar a porta do cliente com npm run dev -- -p 3001.


Parte 8 — Onde este cliente encaixa​

Diagrama da arquitetura da disciplina: um bloco central de serviço, com a API REST e a base de dados, e dois clientes ligados a ele por HTTP — um cliente web em navegador e um aplicativo móvel — mostrando que os dois consomem o mesmo conjunto de endpoints
O cliente web construído a partir desta aula é o primeiro dos dois consumidores do mesmo serviço. O que os mantém independentes é o contrato da API, não o código compartilhado.

A Figura 3 é a mesma das aulas 2 e 4, e agora deixa de ser plano e vira trabalho. Repare no que ela não mostra: nenhuma linha ligando o cliente web ao cliente mobile, e nenhuma ligando qualquer um deles ao banco. O único ponto de contato é a API.

Isso tem uma consequência concreta para esta aula. O acervo que você vai declarar no laboratório é fixo e preserva um recorte dos campos de cada livro da aula 9, incluindo autor e editora. Ele não reproduz a resposta HTTP inteira: GET /livros devolve { dados, pagina, tamanho, total, totalDePaginas }, e os registros têm outros campos, como autorId e editoraId. O detalhe ainda inclui categorias.

Nesta aula, as funções locais devolvem diretamente um vetor e um livro. Na aula 13 será preciso aguardar as chamadas HTTP, extrair dados e tratar falhas; a paginação fica para a aula 14. Manter nomes e nulabilidade dos campos facilita reaproveitar os componentes, mas não elimina essas mudanças nas páginas.

Consumo da API: apenas conceitual nesta aula

O contrato define os dados que um cliente poderá receber do serviço. Essa relação orientará a integração na aula 13. Nesta aula, basta compreender o papel da API na arquitetura; toda a prática usa dados locais, sem executar o backend, consultar endpoints ou implementar chamadas HTTP.


Parte 9 — O que transfere das aulas anteriores​

Você não está começando do zero. Boa parte do Módulo 2 vale aqui:

Vem das aulas anterioresComo aparece no Next.js
TypeScript (aula 3)mesma linguagem e sistema de tipos; o tsconfig.json é configurado para o Next.js
Pensar em contrato antes de implementação (aula 4)o tipo Livro do cliente nasce da resposta da API
Separar transporte de regra (aula 5)a página monta a tela; quem sabe de dados é outro módulo
Status HTTP (aula 4)notFound() sinaliza ausência; o status depende de a resposta já ter começado

E o que não transfere, para você não procurar:

  • O Next.js não fornece o container de injeção de dependência do NestJS. Lá, um módulo declara o que fornece e o container monta o grafo. No Next.js, um componente simplesmente importa o que precisa. A composição é feita por importação e por props.
  • Não há arquivo de rotas nem decorator de rota. O @Controller('livros') do NestJS vira o nome de uma pasta.
  • Uma página não recebe o objeto de requisição do NestJS. Ela recebe params e searchParams. No servidor, APIs como cookies() e headers() permitem consultar outros dados da requisição; serão retomadas depois.

Erros comuns​

ErroSintomaCorreção
onClick ou useState em componente de servidorerro sobre handler não serializável ou hook que exige componente de clienteextrair o trecho interativo para um arquivo com "use client"
Acessar params.id sem awaiterro de acesso à Promise; pode causar falha de renderização ou um falso 404const { id } = await params
Comparar id textual com númeronenhum registro casa, sem erro nenhumconverter: Number(id)
"use client" depois de imports ou outro códigonão estabelece uma diretiva válidacolocar no início, antes dos imports; comentários podem vir antes
"use client" no layout raiz deste laboratórioconflito com a exportação de metadatamanter o layout no servidor e extrair a interatividade
<a href="/livros"> no lugar de <Link>navegação de documento completo, reiniciando o estado Reactusar <Link> para endereços internos
Pasta criada sem page.tsxa URL responde 404acrescentar page.tsx no segmento
Seguir tutorial do Pages RoutergetServerSideProps e afins simplesmente não são chamadosconferir se o material usa app/
Rodar next lintcomando não existe maisnpm run lint, que chama o ESLint diretamente
Uma tela de ausência também pode esconder um erro

Um tipo escrito manualmente não transforma o valor recebido em execução. Inspecione o terminal, o editor e a saída do build: dependendo da versão e do modo, o acesso incorreto a params pode interromper a renderização ou resultar em um falso 404. Os tipos de rota gerados ajudam a detectar o problema.


Laboratório 11 — O primeiro cliente web​

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

Resultado ao fim deste laboratório: três rotas — página inicial, listagem e detalhe —, um layout compartilhado com navegação, dois componentes próprios (um de servidor e um de cliente) e uma resposta 404 tratada pelo framework. Os dados são fixos, declarados no próprio projeto. O laboratório funciona inteiramente com esse acervo local; o serviço do Módulo 2 pode ficar desligado.

Passo 1 — Conferir o ambiente​

node --version # precisa ser 20.9 ou superior; a disciplina usa 22
npm --version

Se a versão do Node for anterior à 20.9, atualize antes de continuar. É o mesmo Node do Módulo 2, então provavelmente já está pronto.

Passo 2 — Criar o projeto​

npx create-next-app@latest biblioteca-web

Se o npm pedir confirmação para instalar o gerador, confirme. Na pergunta “Would you like to use the recommended Next.js defaults?”, selecione “Yes, use recommended defaults”. Siga com as configurações padrão.

O gerador prepara o projeto com TypeScript, ESLint, Tailwind CSS e App Router. @latest usa a versão estável atual; confira as versões instaladas em package.json, pois elas podem mudar após a revisão desta aula. Preserve o package-lock.json gerado para repetir a instalação com npm ci.

Opções do comando — apenas para conhecimento

As opções abaixo permitem personalizar a criação de outros projetos. Neste laboratório, use o comando simples e selecione os defaults; não é necessário acrescentar essas opções.

OpçãoO que decide
--tsprojeto em TypeScript
--eslintverificação de estilo com ESLint
--tailwindTailwind CSS para estilo, por classes utilitárias
--appApp Router — o roteador desta disciplina
--no-src-dirapp/ na raiz, como aparece na documentação oficial
--import-alias "@/*"@/lib/livros em vez de ../../lib/livros
--use-npmnpm como gerenciador de pacotes
--react-compiler / --no-react-compilerativa ou desativa o React Compiler, uma otimização de componentes

Veja outras opções na referência do create-next-app.

cd biblioteca-web
npm run dev

Abra http://localhost:3000. Deve aparecer a página de boas-vindas do Next.js. Deixe o comando rodando: ele observa os arquivos e recarrega o navegador a cada gravação — e é nesse terminal que aparecem os avisos que não chegam à tela.

Passo 3 — Ler o esqueleto antes de mexer​

Antes de escrever qualquer coisa, abra os arquivos de app/ e responda para si mesmo:

  1. Em app/layout.tsx, onde estão <html> e <body>, e o que a prop children recebe?
  2. Em app/page.tsx, o que faz dele a página de / — o nome do arquivo, a pasta, ou alguma linha dentro dele?
  3. Em app/globals.css, o que a primeira linha (@import "tailwindcss") traz?
  4. Em package.json, quais são os quatro comandos disponíveis?

Esse exercício de leitura vale mais do que parece: o esqueleto é pequeno o bastante para ser inteiramente compreendido, e é a última vez no semestre em que isso será verdade.

Sobre o AGENTS.md que apareceu

O gerador pode incluir AGENTS.md e CLAUDE.md na raiz do projeto. São instruções para assistentes de código, não componentes nem rotas. Sua presença não é necessária para executar os exemplos.

Passo 4 — Limpar o esqueleto​

Substitua a página de boas-vindas e seus estilos. Os SVGs iniciais de public/ podem ser removidos depois que a página deixar de usá-los; essa pasta serve arquivos estáticos pela raiz da URL.

O layout completo do passo 6 já remove next/font/google e as fontes Geist, evitando essa dependência de rede durante o build. Substitua também app/globals.css inteiro por:

app/globals.css
@import "tailwindcss";

:root {
color-scheme: light dark;
--background: #ffffff;
--foreground: #171717;
}

@media (prefers-color-scheme: dark) {
:root {
--background: #0a0a0a;
--foreground: #ededed;
}
}

body {
background: var(--background);
color: var(--foreground);
font-family: Arial, Helvetica, sans-serif;
}

Substitua app/page.tsx inteiro:

app/page.tsx
import Link from "next/link";

export default function Page() {
return (
<section>
<h1 className="text-2xl font-semibold">Biblioteca</h1>
<p className="mt-3 max-w-prose">
Cliente web do acervo.
</p>
<p className="mt-4">
<Link href="/livros" className="underline">
Ver o acervo
</Link>
</p>
</section>
);
}

Salve e olhe o navegador: a página trocou sozinha, sem recarregar. O link ainda leva a lugar nenhum — /livros não existe.

Passo 5 — Declarar o acervo​

Crie lib/livros.ts, fora de app/, para separar o acesso a dados dos arquivos de interface. É uma escolha de organização: módulos auxiliares também podem ficar dentro de app/, como o componente do passo 7.

lib/livros.ts
export type Autor = {
id: number;
nome: string;
nacionalidade: string | null;
};

export type Editora = {
id: number;
nome: string;
};

export type Livro = {
id: number;
titulo: string;
isbn: string;
ano: number;
autor: Autor;
// A propriedade existe em todos os livros; `null` indica editora não informada.
editora: Editora | null;
};

Em seguida, no mesmo arquivo, declare três livros e duas funções para consultar esse vetor local:

lib/livros.ts
const machado: Autor = { id: 1, nome: "Machado de Assis", nacionalidade: "Brasileira" };
const clarice: Autor = { id: 2, nome: "Clarice Lispector", nacionalidade: "Brasileira" };
const record: Editora = { id: 1, nome: "Record" };

const livros: Livro[] = [
{ id: 1, titulo: "Dom Casmurro", isbn: "9788525406958", ano: 1899, autor: machado, editora: null },
{ id: 2, titulo: "Memórias Póstumas de Brás Cubas", isbn: "9788535914849", ano: 1881, autor: machado, editora: null },
{ id: 3, titulo: "A Hora da Estrela", isbn: "9788520925829", ano: 1977, autor: clarice, editora: record },
];

export function listarLivros(): Livro[] {
return livros;
}

export function buscarLivro(id: number): Livro | undefined {
return livros.find((livro) => livro.id === id);
}

Os IDs do exemplo são identificadores fixos do acervo local. O tipo Livro descreve os campos usados pela interface, e as duas funções consultam apenas o vetor declarado nesse arquivo.

Dois detalhes deliberados: dois dos três livros estão sem editora, para que a tela seja obrigada a tratar o nulo; e buscarLivro devolve undefined em vez de lançar erro, porque quem decide o que fazer com a ausência é a página, não este módulo.

No seu projeto

Escolha uma entidade do seu domínio, defina seu tipo e declare alguns registros fixos em um arquivo local. Inclua um campo anulável para praticar sua exibição. Diferencie campo ausente (campo?: Tipo) de campo presente e anulável (campo: Tipo | null). Aqui, editora sempre existe, mas pode ser null.

Passo 6 — O layout raiz​

Substitua todo o conteúdo de app/layout.tsx por um cabeçalho de navegação e a área de conteúdo:

app/layout.tsx
import type { Metadata } from "next";
import Link from "next/link";
import "./globals.css";

export const metadata: Metadata = {
title: "Biblioteca",
description: "Cliente web do acervo da biblioteca.",
};

export default function RootLayout({ children }: LayoutProps<"/">) {
return (
<html lang="pt-BR" className="h-full antialiased">
<body className="flex min-h-full flex-col">
<header className="border-b border-black/10 dark:border-white/15">
<nav className="mx-auto flex max-w-3xl items-baseline gap-6 p-4">
<Link href="/" className="font-semibold">
Biblioteca
</Link>
<Link href="/livros" className="text-sm hover:underline">
Acervo
</Link>
</nav>
</header>

{/* `children` é a página do segmento atual — ou outro layout aninhado. */}
<main className="mx-auto w-full max-w-3xl flex-1 p-4">{children}</main>

<footer className="mx-auto w-full max-w-3xl p-4 text-xs opacity-70">
Laboratório da aula 11
</footer>
</body>
</html>
);
}

Três coisas a observar:

  • lang="pt-BR" não é detalhe: é o que diz ao leitor de tela em que idioma ler a página.
  • metadata é lido pelo Next.js e vira <title> e <meta> no HTML. Toda rota que não declarar os seus herda estes.
  • As classes (flex, max-w-3xl, p-4) são do Tailwind e valem para estilo apenas. Elas não têm papel nenhum no que esta aula ensina — leia-as como enfeite e siga adiante.

Passo 7 — O primeiro componente próprio​

Crie app/components/LivroCard.tsx:

app/components/LivroCard.tsx
import Link from "next/link";
import type { Livro } from "@/lib/livros";

export default function LivroCard({ livro }: { livro: Livro }) {
return (
<article className="rounded-lg border border-black/10 p-4 dark:border-white/15">
<h2 className="font-medium">
<Link href={`/livros/${livro.id}`} className="hover:underline">
{livro.titulo}
</Link>
</h2>
<p className="mt-1 text-sm opacity-80">
{livro.autor.nome} — {livro.ano}
</p>
</article>
);
}

Um componente é uma função que recebe props e devolve marcação. Este recebe um Livro e devolve um cartão — nada mais. Como não tem estado nem evento, ele continua sendo componente de servidor, e nenhuma linha dele será enviada ao navegador. A aula 12 trata desse assunto a fundo.

Repare também onde o arquivo está: dentro de app/, mas em uma pasta sem page.tsx. Nenhuma URL foi criada.

Passo 8 — A rota /livros​

Crie app/livros/page.tsx:

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

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

export default function Page() {
const livros = listarLivros();

return (
<section>
<h1 className="text-2xl font-semibold">Acervo</h1>
<p className="mt-2 text-sm opacity-80">
{livros.length} títulos cadastrados.
</p>

<ul className="mt-4 space-y-3">
{livros.map((livro) => (
<li key={livro.id}>
<LivroCard livro={livro} />
</li>
))}
</ul>
</section>
);
}

A rota passou a existir no instante em que o arquivo foi salvo. Não há registro a fazer em lugar nenhum.

O key={livro.id} não é decoração: é como o React identifica cada item entre duas renderizações. Sem ele, o React emite um aviso, e listas que mudam de ordem podem reutilizar o estado do item errado. Prefira um identificador estável a usar a posição no vetor.

Passo 9 — A rota dinâmica /livros/[id]​

Crie app/livros/[id]/page.tsx — a pasta se chama [id], com colchetes no nome mesmo:

app/livros/[id]/page.tsx
import Link from "next/link";
import { notFound } from "next/navigation";
import { buscarLivro } from "@/lib/livros";

export default async function Page({ params }: PageProps<"/livros/[id]">) {
const { id } = await params;
// Aceita somente a representação decimal de um inteiro positivo.
if (!/^[1-9]\d*$/.test(id) || !Number.isSafeInteger(Number(id))) {
notFound();
}

const livro = buscarLivro(Number(id));

if (!livro) {
notFound();
}

return (
<article>
<p className="text-sm">
<Link href="/livros" className="underline">
Acervo
</Link>
</p>

<h1 className="mt-2 text-2xl font-semibold">{livro.titulo}</h1>

<dl className="mt-4 grid grid-cols-[8rem_1fr] gap-y-2 text-sm">
<dt className="opacity-70">Autor</dt>
<dd>{livro.autor.nome}</dd>

<dt className="opacity-70">Ano</dt>
<dd>{livro.ano}</dd>

<dt className="opacity-70">ISBN</dt>
<dd className="font-mono">{livro.isbn}</dd>

<dt className="opacity-70">Editora</dt>
<dd>{livro.editora ? livro.editora.nome : "não informada"}</dd>
</dl>
</article>
);
}

Confira as duas pontas: /livros/1 mostra Dom Casmurro com editora "não informada"; /livros/3 mostra A Hora da Estrela pela Record.

A validação recusa valores como abc, 1.5 e 1e0; converter com Number() sozinho aceitaria formatos que não adotamos como IDs na URL.

notFound() interrompe a renderização do segmento e mostra a interface de ausência. Em respostas ainda não transmitidas, o status é 404. Se o servidor já iniciou o streaming (envio progressivo da resposta), o status pode permanecer 200, pois os cabeçalhos já foram enviados. O Next.js também inclui a instrução noindex para buscadores. Confira o status na aba de rede do navegador; ver a mensagem de ausência não comprova, sozinho, o código HTTP.

Passo 10 — A página de 404​

Visite /livros/99. A página de "não encontrado" padrão do Next.js aparece, em inglês. Crie app/not-found.tsx para substituí-la:

app/not-found.tsx
import Link from "next/link";

export default function NotFound() {
return (
<section>
<h1 className="text-2xl font-semibold">Página não encontrada</h1>
<p className="mt-3">A página ou o livro solicitado não foi encontrado.</p>
<p className="mt-4">
<Link href="/livros" className="underline">
Voltar ao acervo
</Link>
</p>
</section>
);
}

Confira os dois caminhos que levam a ela: /livros/99, onde quem chama é o seu notFound(), e /nao-existe, que não corresponde a uma página. O mesmo arquivo atende os dois, dentro do layout raiz.

Passo 11 — Experimento: um evento em componente de servidor​

Este passo existe para dar errado. Em app/components/LivroCard.tsx, acrescente um botão dentro do <article>:

<button type="button" onClick={() => alert(livro.titulo)}>
Detalhes
</button>

Recarregue /livros. A página falha, e o terminal do npm run dev mostra:

⨯ Error: Event handlers cannot be passed to Client Component props.
<button type="button" onClick={function onClick} children=...>
^^^^^^^^^^^^^^^^^^
If you need interactivity, consider converting part of this to a Client Component.

A mensagem descreve exatamente o que a Figura 1 mostra: um handler comum não atravessa a fronteira como prop. O componente foi renderizado no servidor e o onClick não tem como ser serializado e enviado.

Agora extraia a interatividade para um componente pequeno. Marcar o LivroCard também funcionaria neste caso, mas o botão isolado permite manter a formatação do cartão no servidor. Remova o botão acrescentado e crie app/components/BotaoCopiarIsbn.tsx:

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

export default function BotaoCopiarIsbn({ isbn }: { isbn: string }) {
async function copiarIsbn() {
try {
await navigator.clipboard.writeText(isbn);
alert("ISBN copiado.");
} catch {
alert("Não foi possível copiar. Selecione o ISBN na página e copie manualmente.");
}
}

return (
<button
type="button"
onClick={copiarIsbn}
className="rounded-md border border-black/15 px-3 py-1 text-sm hover:bg-black/5 dark:border-white/20 dark:hover:bg-white/10"
>
Copiar ISBN
</button>
);
}

A escrita na área de transferência devolve uma Promise e pode falhar por permissões ou indisponibilidade da API. O try/catch trata essa falha; o alert é um retorno provisório, sem introduzir estado antes da aula 12. Use localhost ou HTTPS: um endereço HTTP da rede local pode não permitir acesso à área de transferência.

Importe o componente no topo da página de detalhe:

app/livros/[id]/page.tsx
import BotaoCopiarIsbn from "@/app/components/BotaoCopiarIsbn";

Dentro do return, depois de </dl> e antes de </article>, insira:

<div className="mt-6">
<BotaoCopiarIsbn isbn={livro.isbn} />
</div>

Repare no que atravessou a fronteira: o texto do ISBN, que é serializável. A função ficou inteira do lado do cliente, no arquivo que a declara. E o LivroCard, que não precisa de interatividade, continua sendo componente de servidor.

Duas razões independentes justificam a diretiva neste arquivo: o onClick e o navigator.clipboard, que só existe no navegador. Qualquer uma delas bastaria.

Passo 12 — Experimento: params sem await​

Este passo também introduz um erro deliberado. Primeiro, mantenha PageProps<"/livros/[id]"> e substitua apenas await params por params:

// Somente leitura: alteração temporária dentro da página existente.
const { id } = params;

O editor deve indicar que id não existe no tipo Promise. Se não indicar, salve os arquivos e execute npx next typegen e npx tsc --noEmit.

Agora compare com esta assinatura incorreta encontrada em material antigo:

// Somente leitura: assinatura incorreta para Next.js 16.
export default function Page({ params }: { params: { id: string } }) {
const { id } = params;
// ...restante da página
}

Aplicar essa assinatura pode esconder o erro na linha de acesso, mas não muda o valor entregue pelo framework. Os tipos gerados pelo Next.js e a verificação do build podem rejeitar a assinatura. Em execução, o acesso síncrono é inválido: procure o diagnóstico de Promise no terminal e na tela de desenvolvimento. A apresentação varia conforme a versão e o modo.

Se o acesso resultar em undefined, a validação do ID do passo 9 chama notFound(), produzindo uma falsa ausência de livro. Isso é um defeito do código da página, não uma confirmação de que o registro inexiste.

Desfaça todas as alterações do experimento: restaure PageProps, a função async e const { id } = await params antes de continuar.

Passo 13 — O build​

Pare o npm run dev e rode:

npm run lint
npm run build

Confira o mapa de rotas no fim da saída: /, /_not-found e /livros marcadas com ○, e /livros/[id] com ƒ. Responda para si mesmo por que a listagem é estática e o detalhe não é — a resposta está na Parte 7 e é a pergunta que a aula 13 vai retomar.

Depois, npm run start sobe o resultado do build. Se a porta 3000 estiver ocupada, use npm run start -- -p 3001. Visite novamente as rotas e teste o botão copiando o ISBN para um campo de texto. É essa versão que vai para o ar, não a do npm run dev.

Critérios de conclusão​

O laboratório está completo quando:

  • npm run dev sobe sem erros e sem avisos;
  • / mostra a página inicial, com o cabeçalho de navegação;
  • /livros lista os três títulos, cada um com autor e ano;
  • as telas consultam somente o vetor local e funcionam com o backend desligado;
  • /livros/1 mostra editora "não informada" e /livros/3 mostra "Record";
  • /livros/99, /livros/abc, /livros/1.5 e /nao-existe mostram a interface de ausência, dentro do layout;
  • navegar pelo cabeçalho não recarrega a página inteira;
  • o LivroCard não tem "use client", e o BotaoCopiarIsbn tem;
  • o botão copia o ISBN ou informa a falha; você conferiu o texto copiado;
  • você reproduziu os experimentos dos passos 11 e 12, conferiu os diagnósticos e desfez as alterações;
  • npm run lint termina sem apontamentos;
  • npm run build termina sem erro e você sabe explicar as marcas ○ e ƒ do mapa de rotas;
  • o mesmo esqueleto existe no seu projeto, com o seu domínio.

Fechamento​

A primeira interface está pronta, apoiada em três decisões que valem para o resto do módulo.

A primeira é onde o código roda. Componente de servidor é o padrão, e a diretiva "use client" é a exceção que se pede explicitamente, no menor componente possível. A fronteira segue os imports de execução e permite compor componentes de servidor e de cliente.

A segunda é onde cada arquivo mora. A pasta é a URL e o nome é o papel. Não há registro de rotas para consultar: para saber o que a aplicação publica, basta olhar a árvore de app/.

A terceira é de onde vêm os dados. As páginas do laboratório leem de um módulo que devolve um vetor fixo. Isso permite estudar rotas, layouts e componentes com dados disponíveis no próprio projeto.

A aula 12 volta um passo, para o React que sustenta tudo isso: componentes, props, estado e eventos — o conteúdo do BotaoCopiarIsbn, que aqui foi usado antes de ser explicado.


Exercícios (checkpoints)​

  1. Liste três responsabilidades que o Next.js assume e que você teria de resolver sozinho usando apenas o React. Para cada uma, descreva uma consequência concreta de resolvê-la mal.

  2. Decida, para cada requisito, se o componente pode permanecer no servidor ou precisa de interatividade no cliente, e justifique em uma frase: (a) exibir a ficha de um livro; (b) um campo de busca que filtra a lista enquanto se digita; (c) um rodapé com texto fixo; (d) um botão que guarda o último livro visitado no localStorage.

  3. Considere os arquivos app/layout.tsx, app/page.tsx, app/autores/page.tsx, app/autores/[id]/page.tsx, app/autores/[id]/layout.tsx e app/lib/formatar.ts. Escreva os padrões de URL publicados e aponte os arquivos que não publicam uma tela por conta própria.

  4. Diagnostique: uma página usa const { id } = params sem await e exibe uma falsa ausência de livro. Explique por que um tipo manual não corrige o valor recebido e indique como os tipos gerados, o terminal e o build ajudam a detectar o erro.

  5. Explique por que adicionar "use client" ao layout do laboratório conflita com metadata. Se essa exportação fosse removida, distinga o efeito sobre imports de execução do efeito sobre componentes de servidor recebidos por children.

  6. Compare <Link href="/livros"> com <a href="/livros"> em uma aplicação Next.js: descreva o que muda na experiência de quem clica e indique uma situação em que o <a> é a escolha correta.

  7. Implemente no laboratório uma rota /autores/[id] que mostre o nome do autor e a lista dos livros dele, reaproveitando o LivroCard. Indique que arquivos você criou e por que cada um está onde está.

  8. Acrescente um livro ao vetor local com editora: null. Confira sua presença na listagem e na página de detalhe e explique como cada página obtém os dados sem precisar de um serviço externo.


Referências​

Principais​

Aprofundamento​