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 depois | Onde |
|---|---|
| Componentes, props, estado, eventos e composição | Aula 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ção | Aula 13 — Dados da API |
| Formulários, envio de dados, paginação e cache | Aula 14 — Consumo de serviços no frontend |
| Login, sessão, rotas protegidas e publicação | Aula 15 — Sessão e publicação |
| O mesmo serviço consumido por outro cliente | Mó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-founde 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
paramsacessado semawait. - Ler a saída de
next deve denext 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.
| Ferramenta | Versão | Observação |
|---|---|---|
| Node.js | 22 LTS ou superior | node --version; o Next.js 16 exige 20.9 no mínimo |
| npm | a que acompanha o Node | npm --version |
| Next.js | 16.x | exemplos verificados com 16.3.4; o comando com @latest instala a versão estável atual |
| React | 19.2.x | instalado pelo create-next-app; confira package.json e preserve package-lock.json |
| TypeScript | 5.x | mínimo 5.1 |
| Navegador | Chrome, Edge, Firefox ou Safari recentes | o Next.js 16 tem como alvo Chrome 111+, Firefox 111+ e Safari 16.4+ |
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:
| Pergunta | Quem 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.
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 acrescenta | Em uma frase |
|---|---|
| Roteamento por sistema de arquivos | a estrutura de pastas de app/ é o mapa de URLs; não existe arquivo de rotas |
| Renderização no servidor | por 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 empacotamento | Turbopack, já configurado, com recarga rápida em desenvolvimento |
| Otimizações de entrega | imagens, fontes e scripts tratados por componentes próprios |
| Camada de dados e cache | busca 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.
A Figura 1 responde à pergunta prática: quando marcar um componente com
"use client"?
| Precisa de | Onde 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 privadas | servidor; neste curso, por meio da API NestJS |
| Usar chave ou segredo que não deve ser público | servidor, sem expor o valor na resposta |
| Só formatar e exibir dados que recebeu | pode 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.
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.
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:
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 éasynce o valor sai de umawait. 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"com3nã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.
| Arquivo | O que faz | Onde a disciplina trata |
|---|---|---|
page.tsx | publica uma tela em um endereço | aula 11 |
layout.tsx | envolve o segmento e o que estiver abaixo; preserva estado na navegação | aulas 11 e 13 |
not-found.tsx | resposta para rota inexistente ou para uma chamada a notFound() | aula 11 |
loading.tsx | o que mostrar enquanto o conteúdo do segmento carrega | aula 13 |
error.tsx | o que mostrar quando o segmento lança uma exceção | aula 13 |
template.tsx | como o layout, mas reconstruído a cada navegação | fora do escopo |
route.ts | expõe um endpoint HTTP em vez de uma tela | aula 14 |
default.tsx | conteúdo padrão de rotas paralelas | fora do escopo |
proxy.ts | intercepta requisições antes de chegarem à rota (fora de app/) | aula 15 |
middleware.tsEra 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.
Parte 6 — Navegação: <Link>, e não <a>
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.
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
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.
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 anteriores | Como 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
paramsesearchParams. No servidor, APIs comocookies()eheaders()permitem consultar outros dados da requisição; serão retomadas depois.
Erros comuns
| Erro | Sintoma | Correção |
|---|---|---|
onClick ou useState em componente de servidor | erro sobre handler não serializável ou hook que exige componente de cliente | extrair o trecho interativo para um arquivo com "use client" |
Acessar params.id sem await | erro de acesso à Promise; pode causar falha de renderização ou um falso 404 | const { id } = await params |
Comparar id textual com número | nenhum registro casa, sem erro nenhum | converter: Number(id) |
"use client" depois de imports ou outro código | não estabelece uma diretiva válida | colocar no início, antes dos imports; comentários podem vir antes |
"use client" no layout raiz deste laboratório | conflito com a exportação de metadata | manter o layout no servidor e extrair a interatividade |
<a href="/livros"> no lugar de <Link> | navegação de documento completo, reiniciando o estado React | usar <Link> para endereços internos |
Pasta criada sem page.tsx | a URL responde 404 | acrescentar page.tsx no segmento |
| Seguir tutorial do Pages Router | getServerSideProps e afins simplesmente não são chamados | conferir se o material usa app/ |
Rodar next lint | comando não existe mais | npm run lint, que chama o ESLint diretamente |
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.
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ção | O que decide |
|---|---|
--ts | projeto em TypeScript |
--eslint | verificação de estilo com ESLint |
--tailwind | Tailwind CSS para estilo, por classes utilitárias |
--app | App Router — o roteador desta disciplina |
--no-src-dir | app/ na raiz, como aparece na documentação oficial |
--import-alias "@/*" | @/lib/livros em vez de ../../lib/livros |
--use-npm | npm como gerenciador de pacotes |
--react-compiler / --no-react-compiler | ativa 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:
- Em
app/layout.tsx, onde estão<html>e<body>, e o que a propchildrenrecebe? - Em
app/page.tsx, o que faz dele a página de/— o nome do arquivo, a pasta, ou alguma linha dentro dele? - Em
app/globals.css, o que a primeira linha (@import "tailwindcss") traz? - 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.
AGENTS.md que apareceuO 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:
@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:
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.
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:
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.
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:
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:
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:
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:
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:
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:
"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:
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 devsobe sem erros e sem avisos; -
/mostra a página inicial, com o cabeçalho de navegação; -
/livroslista 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/1mostra editora "não informada" e/livros/3mostra "Record"; -
/livros/99,/livros/abc,/livros/1.5e/nao-existemostram a interface de ausência, dentro do layout; - navegar pelo cabeçalho não recarrega a página inteira;
- o
LivroCardnão tem"use client", e oBotaoCopiarIsbntem; - 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 linttermina sem apontamentos; -
npm run buildtermina 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)
-
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.
-
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. -
Considere os arquivos
app/layout.tsx,app/page.tsx,app/autores/page.tsx,app/autores/[id]/page.tsx,app/autores/[id]/layout.tsxeapp/lib/formatar.ts. Escreva os padrões de URL publicados e aponte os arquivos que não publicam uma tela por conta própria. -
Diagnostique: uma página usa
const { id } = paramssemawaite 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. -
Explique por que adicionar
"use client"ao layout do laboratório conflita commetadata. Se essa exportação fosse removida, distinga o efeito sobre imports de execução do efeito sobre componentes de servidor recebidos porchildren. -
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. -
Implemente no laboratório uma rota
/autores/[id]que mostre o nome do autor e a lista dos livros dele, reaproveitando oLivroCard. Indique que arquivos você criou e por que cada um está onde está. -
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
- Next.js — Installation — requisitos,
create-next-appe estrutura inicial - Next.js — Layouts and Pages — rotas, layouts, segmentos dinâmicos e
<Link> - Next.js — Server and Client Components — a fronteira,
"use client"e composição - Next.js — Project structure — a lista completa dos arquivos especiais
- React — Your First Component — leitura de apoio para a aula 12
Aprofundamento
- Next.js — Migração para a versão 16 — remoção do acesso síncrono às APIs de requisição
- Next.js — not-found — interface de ausência e status em respostas com streaming
- Next.js — create-next-app — opções do gerador
- Next.js 16 — anúncio da versão — o que mudou, incluindo
paramsassíncrono e Turbopack por padrão - Next.js — The Server and Client Boundary — o que acontece na renderização, em detalhe
- Next.js — Linking and Navigating — antecipação, transições e navegação no cliente
- Next.js — Turbopack — o bundler padrão e como voltar ao webpack
- React — Server Components — o mecanismo, na fonte