// rotas, Server Components e data fetching
O App Router veio para ficar: pastas viram rotas, layouts compartilhados, loading e error automáticos. Este guia cobre do básico às boas práticas — seja você júnior entrando no App Router ou pleno/sênior consolidando decisões de arquitetura. Exemplos prontos para copiar e colar.
// finalmente um router que não esconde as pastas
No App Router, a estrutura de pastas em app/ define as rotas: cada pasta vira um segmento da URL (ex.: app/dashboard vira /dashboard). O layout.tsx envolve a rota e seus filhos — define o "casco" da página (sidebar, header). O page.tsx é o conteúdo que aparece naquele path.
Pense em layout como o template que se repete em todas as páginas daquele segmento; page é o que muda de rota para rota.
export default function DashboardLayout({children,}: {children: React.ReactNode;}) {return (<div className="dashboard"><aside>Menu lateral</aside><main>{children}</main></div>);}
export default function DashboardPage() {return <h1>Dashboard</h1>;}// URL: /dashboard
Pasta entre parênteses (ex.: (auth)) não vira segmento na URL — útil para agrupar login e cadastro sob o mesmo layout sem poluir a URL. Convenções: layout.tsx e page.tsx são nomes reservados; só um page por pasta.
// um layout para tudo ou um por segmento
O app/layout.tsx envolve toda a aplicação (html, body, providers). Você pode criar layout.tsx dentro de qualquer pasta (ex.: app/dashboard/layout.tsx): esse layout vale só para as rotas daquele segmento. Assim você controla o que é global (header/footer da raiz) e o que muda por área (sidebar só no dashboard).
Use layout por pasta quando uma área tiver UI própria: dashboard com sidebar, área logada com menu diferente, blog só com header. Layouts são aninhados: o da raiz envolve o do dashboard, que por sua vez envolve a page.
Clique nos itens para ver as descrições:
Pasta raiz das rotas
Cada layout.tsx envolve apenas as page.tsx da sua pasta e subpastas. O layout da raiz sempre envolve os outros (não dá para "pular" o layout global). Troque de layout trocando de rota.
// use client só quando precisar de interatividade
Por padrão, todo componente no App Router é um Server Component: roda no servidor, não aumenta o bundle do cliente e pode acessar dados direto (DB, APIs internas). Use "use client" no topo do arquivo quando precisar de useState, useEffect, event handlers ou browser APIs (localStorage, window).
Menos "use client" = menos JavaScript no cliente = página mais rápida e melhor para SEO. Use Server para listagens, dados e estrutura; Client só para o que reage a clique, tempo ou input do usuário. Você pode importar um Client Component dentro de um Server Component — o Server renderiza o casco e o Client hidrata onde há interatividade.
async function PostList() {const posts = await fetch('https://api.example.com/posts').then(r => r.json());return (<ul>{posts.map(p => <li key={p.id}>{p.title}</li>)}</ul>);}
"use client";import { useState } from 'react';export function LikeButton() {const [liked, setLiked] = useState(false);return (<button onClick={() => setLiked(!liked)}>{liked ? 'Curtido' : 'Curtir'}</button>);}
Regra de ouro: comece como Server; adicione "use client" só nos componentes que realmente precisam de interatividade. Evite "use client" na raiz da página — deixe no menor componente possível (ex.: só no botão de like, não na lista inteira).
// fetch com cache e revalidação
Em Server Components você pode usar fetch direto — sem useEffect nem lib externa. O Next.js estende o fetch com cache: por padrão as requisições são cacheadas, o que evita chamadas repetidas e deixa a navegação entre rotas mais rápida quando os dados já foram buscados.
Escolha a estratégia pelo tipo de dado: revalidate em segundos para dados que podem ficar um pouco desatualizados (listagens, catálogo); cache: 'no-store' para dados em tempo real (carrinho, saldo). Em desenvolvimento (npm run dev) o cache pode se comportar diferente; em produção o revalidate funciona como esperado.
export default async function ProdutosPage() {const res = await fetch('https://api.loja.com/produtos', {next: { revalidate: 60 },});const produtos = await res.json();return (<ul>{produtos.map(p => <li key={p.id}>{p.nome}</li>)}</ul>);}
next: { revalidate: 60 } revalida a resposta a cada 60 segundos. Para dados que mudam a cada request (ou não devem ser cacheados), use cache: 'no-store'. fetch() em Server Components é automático por request em rotas dinâmicas.
// UX sem tela em branco
loading.tsx mostra um fallback enquanto o conteúdo da rota carrega (Streaming): o usuário vê o loading daquele segmento e o resto da página pode já estar visível. error.tsx captura erros naquele segmento e abaixo, e permite mostrar uma UI de erro em vez de quebrar a página inteira. São convenções: basta criar os arquivos no nível da rota desejada.
O usuário pode tentar de novo sem recarregar a página: error.tsx recebe reset() e você chama no botão "Tentar de novo". Como reset é usado em onClick, error.tsx precisa ser Client Component. Coloque loading.tsx e error.tsx na pasta da rota que você quer proteger (ex.: app/dashboard/).
export default function Loading() {return <div>Carregando dashboard...</div>;}
"use client";export default function Error({ error, reset }: {error: Error;reset: () => void;}) {return (<div><p>Algo deu errado.</p><button onClick={reset}>Tentar de novo</button></div>);}
error.tsx precisa ser Client Component porque usa onClick no reset. O boundary captura erros apenas no segmento onde está e nos filhos — erros em layout.tsx da raiz não são capturados por app/dashboard/error.tsx.
Temos TypeScript, React Patterns, Git Workflow e outros guias práticos na seção Dicas.