Next.js App Router para iniciantes: conceitos e uso básico

Na primeira vez que abri a documentação oficial do Next.js, fiquei perdido. Na barra lateral, “Pages Router” e “App Router” apareciam lado a lado, como se dissessem “escolha qualquer um”. Mas aí surgiram as dúvidas: qual escolher? Qual é a diferença entre os dois? A documentação não dava uma resposta direta e parecia ficar mais confusa a cada página. Alguns tutoriais usavam a pasta pages, outros usavam app, e até a forma de escrever o código mudava completamente.
Só depois entendi que o Next.js tem dois sistemas de roteamento bem diferentes. O antigo se chama Pages Router: é estável e confiável, mas não oferece alguns recursos novos. O mais recente é o App Router, lançado a partir da versão 13 e considerado estável desde a 13.4. Hoje, ele é o caminho recomendado oficialmente.
Talvez você esteja pensando: “Ainda preciso aprender o App Router? Será que é só mais uma novidade que vai dar trabalho?”
Este texto busca resolver essa dúvida. Vou explicar de forma simples os conceitos centrais do App Router: o que são Server Components, como usar os arquivos especiais e o que realmente muda em relação ao Pages Router. Ao final, você terá uma base para começar sem tropeçar nos mesmos pontos.
O que é o App Router e por que usá-lo?
Em poucas palavras, o App Router é o sistema de roteamento lançado no Next.js 13. Ele se baseia em um recurso moderno do React, os Server Components, e oferece uma arquitetura mais flexível.
Em comparação com o Pages Router, há três vantagens evidentes:
1. Melhor desempenho
O App Router usa componentes de servidor por padrão. Isso significa que boa parte do código é executada no servidor, reduzindo o JavaScript que o navegador precisa baixar e, consequentemente, acelerando o carregamento. Segundo um relatório da Vercel de 2024, mais de 60% dos principais aplicativos Next.js já haviam migrado para o App Router.
"Mais de 60% dos principais aplicativos Next.js já migraram para o App Router"
2. Sistema de layouts mais flexível
Criar layouts aninhados no Pages Router dá mais trabalho. No App Router, um arquivo layout.js resolve essa estrutura diretamente e o layout não é renderizado novamente a cada troca de página, deixando a navegação mais fluida.
3. Tratamento de erros e estados de carregamento mais completo
Você pode definir a interface de carregamento em loading.js e capturar erros com error.js, exibindo uma interface alternativa. No Pages Router, seria necessário implementar esses comportamentos manualmente; no App Router, eles já fazem parte das convenções do framework.
O Pages Router ainda pode ser usado? Sim.
Os dois sistemas podem coexistir. Mas, se você está começando a estudar Next.js agora, recomendo ir direto para o App Router. Ele é a opção indicada oficialmente e, desde a versão 14.1.4, novos projetos criados pelo scaffold já usam o App Router por padrão.
Roteamento pelo sistema de arquivos: dos diretórios às páginas
O conceito mais importante do App Router é simples: a estrutura de pastas do projeto também é a estrutura das rotas.
Pode parecer abstrato, mas este exemplo deixa tudo mais claro:
app/
├── page.js # Página inicial, corresponde a /
├── about/
│ └── page.js # Página Sobre, corresponde a /about
└── blog/
├── page.js # Lista do blog, corresponde a /blog
└── [slug]/
└── page.js # Detalhe do post, corresponde a /blog/:slug
Há alguns pontos importantes:
1. page.js é a entrada da rota
Somente arquivos chamados page.js se tornam páginas acessíveis. Outros arquivos, como layout.js e loading.js, dão suporte à rota, mas não podem ser acessados diretamente.
2. Rotas dinâmicas usam colchetes
Quer criar uma rota dinâmica como /blog/hello-world? Crie app/blog/[slug]/page.js. O parâmetro slug será passado automaticamente para o componente:
// app/blog/[slug]/page.js
export default function BlogPost({ params }) {
return <h1>Artigo: {params.slug}</h1>
}
3. Rotas catch-all usam [...slug]
Em alguns casos, é preciso capturar vários níveis de caminho, como /docs/a/b/c. Para isso, use app/docs/[...slug]/page.js; params.slug será o array ['a', 'b', 'c'].
Comparação com o Pages Router:
Se você já usou o Pages Router, deve lembrar do caminho pages/blog/[id].js. No App Router, ele vira app/blog/[id]/page.js, com uma pasta adicional. O motivo é reservar espaço em cada rota para arquivos especiais como layout.js e loading.js.
No início, isso pode parecer trabalhoso. Depois que você se acostuma, porém, a estrutura do projeto fica bem mais clara.
Server Components vs. Client Components: o conceito central
Este talvez seja o conceito mais confuso do App Router. Quando comecei a estudá-lo, também levei um tempo para entender.
Em resumo: os componentes do App Router são executados no servidor por padrão e só rodam no navegador quando precisam de interatividade.
O padrão é Server Component
Componentes criados no diretório app/ são Server Components por padrão. Eles são renderizados no servidor, que envia o HTML pronto para o navegador.
As vantagens são claras:
- Menos JavaScript: o código do componente não precisa ser enviado ao navegador, reduzindo o tamanho dos arquivos JS baixados
- Acesso direto a recursos do backend: consultas ao banco de dados e segredos de API podem ser usados no servidor
- Primeiro carregamento mais rápido: o navegador recebe o HTML já renderizado, reduzindo o tempo de FCP (First Contentful Paint)
Este é um Server Component típico:
// app/products/page.js
// Este é um Server Component e é executado no servidor
async function getProducts() {
const res = await fetch('https://api.example.com/products')
return res.json()
}
export default async function ProductsPage() {
const products = await getProducts()
return (
<div>
<h1>Lista de produtos</h1>
{products.map(p => (
<div key={p.id}>{p.name}</div>
))}
</div>
)
}
Percebeu? Você pode usar async/await diretamente para buscar dados, sem useEffect nem getServerSideProps.
Quando usar um Client Component?
Alguns recursos precisam ser executados no navegador, por exemplo:
- React hooks (
useState,useEffect) - Interações do usuário (
onClick,onChange) - APIs do navegador (
localStorage,window)
Nesses casos, você precisa de um Client Component. Para marcá-lo, basta adicionar 'use client' no início do arquivo:
// components/AddToCartButton.js
'use client' // Marca o arquivo como Client Component
import { useState } from 'react'
export default function AddToCartButton({ productId }) {
const [count, setCount] = useState(0)
return (
<button onClick={() => setCount(count + 1)}>
Adicionar ao carrinho ({count})
</button>
)
}
Combinando os dois: prática recomendada
O ponto forte é poder combinar os dois tipos de componente.
Em uma página de produtos, por exemplo:
- Use um Server Component para a lista de produtos (os dados são buscados no servidor e menos JavaScript é enviado ao navegador)
- Use um Client Component para o botão de adicionar ao carrinho (ele precisa tratar o clique)
// app/products/page.js (Server Component)
import AddToCartButton from '@/components/AddToCartButton' // Client Component
async function getProducts() {
// Busca os dados no servidor
}
export default async function ProductsPage() {
const products = await getProducts()
return (
<div>
<h1>Lista de produtos</h1>
{products.map(p => (
<div key={p.id}>
{p.name}
<AddToCartButton productId={p.id} />
</div>
))}
</div>
)
}
Guarde esta regra: use Server Component por padrão. Adicione 'use client' apenas quando a interatividade realmente for necessária.
Se você colocar 'use client' em todos os componentes logo de saída, perde boa parte da vantagem de usar o App Router.
Arquivos especiais que deixam a estrutura mais robusta
O App Router define vários nomes de arquivo especiais, como layout.js, loading.js e error.js. No primeiro contato, eles podem parecer mais uma complicação, mas simplificam bastante o trabalho.
layout.js: layout compartilhado
Este é o arquivo especial mais usado. Ele define o layout de um segmento de rota e envolve todas as páginas no mesmo nível ou abaixo dele.
Se você quiser adicionar uma barra de navegação e um rodapé ao aplicativo inteiro, pode fazer assim:
// app/layout.js (layout raiz)
export default function RootLayout({ children }) {
return (
<html lang="pt-BR">
<body>
<nav>Barra de navegação</nav>
<main>{children}</main>
<footer>Rodapé</footer>
</body>
</html>
)
}
Você também pode aninhar layouts:
app/
├── layout.js # Layout global (navegação + rodapé)
├── page.js # Página inicial
└── dashboard/
├── layout.js # Layout do painel (barra lateral)
├── page.js # /dashboard
└── settings/
└── page.js # /dashboard/settings
Ao navegar de /dashboard para /dashboard/settings, nem o layout global nem o layout do painel são renderizados novamente; apenas o page.js é atualizado. A transição fica mais fluida.
loading.js: estado de carregamento
Você não precisa escrever seu próprio useState para gerenciar o carregamento. Crie um loading.js, e o App Router envolverá a página com Suspense automaticamente:
// app/dashboard/loading.js
export default function Loading() {
return <div>Carregando...</div>
}
Enquanto os dados da página são buscados, o conteúdo de loading.js aparece automaticamente. É só isso.
error.js: limite de erros
Esse arquivo captura erros da página e exibe uma interface alternativa:
// app/dashboard/error.js
'use client' // Error boundaries precisam ser Client Components
export default function Error({ error, reset }) {
return (
<div>
<h2>Ocorreu um erro: {error.message}</h2>
<button onClick={reset}>Tentar novamente</button>
</div>
)
}
Há uma armadilha importante: error.js não captura erros do layout.js no mesmo nível. Isso ocorre por uma limitação dos React Error Boundaries: eles só capturam erros de componentes filhos, não erros do próprio componente nem de seus pais.
Para capturar erros de layout.js, coloque error.js no diretório pai ou use global-error.js no diretório raiz.
not-found.js: página 404
Esse arquivo é exibido quando a rota não existe:
// app/not-found.js
export default function NotFound() {
return <h1>Página não encontrada</h1>
}
Também é possível acionar um erro 404 pelo código:
import { notFound } from 'next/navigation'
export default async function BlogPost({ params }) {
const post = await getPost(params.slug)
if (!post) notFound() // Aciona not-found.js
return <article>{post.title}</article>
}
Relação entre os arquivos
Esses arquivos especiais seguem uma hierarquia fixa:
layout.js
├── loading.js (limite de Suspense)
│ └── page.js
└── error.js (limite de erros)
O layout fica na camada mais externa, por isso error.js não consegue envolvê-lo. loading.js cuida do estado de carregamento, enquanto error.js cuida dos erros.
Depois de entender essa hierarquia, fica bem mais fácil evitar problemas.
Busca de dados sem getServerSideProps
Quem já usou o Pages Router provavelmente escreveu getServerSideProps ou getStaticProps. Para ser sincero, essas APIs não são muito intuitivas: você precisa exportar uma função separada e o fluxo dos dados não é tão direto.
O App Router simplifica esse processo.
Use async/await diretamente
Em um Server Component, você pode buscar dados dentro da própria função do componente:
// app/posts/page.js
async function getPosts() {
const res = await fetch('https://api.example.com/posts')
return res.json()
}
export default async function PostsPage() {
const posts = await getPosts()
return (
<ul>
{posts.map(post => (
<li key={post.id}>{post.title}</li>
))}
</ul>
)
}
É apenas async/await, sem uma API especial.
Busca paralela de dados
Também é possível buscar várias fontes de dados em paralelo:
export default async function Dashboard() {
// Busca em paralelo, sem bloquear uma operação pela outra
const [user, posts, stats] = await Promise.all([
getUser(),
getPosts(),
getStats()
])
return (
<div>
<h1>{user.name}</h1>
<Posts data={posts} />
<Stats data={stats} />
</div>
)
}
Cache e revalidação de dados
O Next.js armazena as solicitações fetch em cache automaticamente. Você pode controlar a estratégia:
// Revalida depois de 60 segundos em cache
fetch('https://api.example.com/data', {
next: { revalidate: 60 }
})
// Não usa cache; busca os dados mais recentes em cada solicitação
fetch('https://api.example.com/data', {
cache: 'no-store'
})
Comparação com o Pages Router:
- Pages Router:
getServerSideProps+getStaticProps, exportados como funções separadas - App Router:
async/awaitdiretamente no componente
Bem mais simples, não é?
Problemas comuns para iniciantes e como resolvê-los
Encontrei várias armadilhas quando comecei a usar o App Router. Estas são algumas das dúvidas mais frequentes e as soluções que podem poupar seu tempo.
Problema 1: quando usar ‘use client’?
A dúvida: os tutoriais mostram 'use client' em vários lugares, mas não fica claro quando ele é necessário.
A solução:
Guarde esta regra: não adicione por padrão; use somente quando precisar.
Adicione 'use client' apenas nestas situações:
- O componente usa React hooks (
useState,useEffect,useContext) - Há interação do usuário (
onClick,onChange) - O componente usa APIs do navegador (
window,localStorage)
Nos outros casos, não adicione a diretiva. Server Components oferecem melhor desempenho e podem acessar recursos do backend diretamente.
Problema 2: qual é a relação entre layout.js e page.js?
A dúvida: os dois arquivos ficam na mesma pasta; qual deles envolve o outro?
A solução:
layout.js envolve page.js e as rotas filhas.
app/
├── layout.js # Envolve todas as páginas abaixo
├── page.js # Página inicial, envolvida pelo layout acima
└── about/
└── page.js # Página Sobre, também envolvida pelo layout acima
Durante a navegação, layout.js não é renderizado novamente; somente page.js é atualizado. Por isso, a barra de navegação não pisca a cada mudança de página.
Problema 3: como acessar parâmetros de rotas dinâmicas?
A dúvida: você criou [slug]/page.js, mas não sabe como obter o valor de slug.
A solução:
Use a propriedade params:
// app/blog/[slug]/page.js
export default function BlogPost({ params }) {
console.log(params.slug) // Este é o valor presente na URL
return <h1>Artigo: {params.slug}</h1>
}
Para uma rota dinâmica aninhada, como app/blog/[category]/[slug]/page.js:
export default function Post({ params }) {
console.log(params.category, params.slug)
return <h1>{params.category} - {params.slug}</h1>
}
Problema 4: error.js não funciona?
A dúvida: você criou error.js, mas ele não captura um erro no layout.
A solução:
error.js não captura erros de layout.js no mesmo nível. Essa é uma limitação dos React Error Boundaries.
Há duas formas de capturar erros de layout:
- Coloque
error.jsno diretório pai - Use
global-error.jsno diretório raiz; ele deve conter as tags<html>e<body>
// app/global-error.js
'use client'
export default function GlobalError({ error, reset }) {
return (
<html>
<body>
<h2>Erro global: {error.message}</h2>
<button onClick={reset}>Tentar novamente</button>
</body>
</html>
)
}
Problema 5: devo migrar meu projeto antigo?
A dúvida: o App Router apresenta muitos conceitos novos, e você teme precisar reescrever todo o projeto antigo.
A solução:
Não há pressa.
Pages Router e App Router podem coexistir. Você pode:
- Manter os recursos antigos em
pages/ - Criar novos recursos em
app/
A Vercel também afirma que o Pages Router continuará recebendo suporte de longo prazo e não será descontinuado.
Para um projeto novo, porém, use o App Router desde o início. Ele é o caminho adotado pelo ecossistema e deve continuar evoluindo.
Conclusão
Depois de todos esses detalhes, vale recapitular os cinco conceitos centrais do App Router:
- Roteamento pelo sistema de arquivos: a estrutura de pastas define as rotas, e
page.jsé o ponto de entrada - Server Components: executados no servidor por padrão, com melhor desempenho
- Client Components: marcados com
'use client'quando há interatividade - Arquivos especiais:
layout.js,loading.jseerror.jstornam a estrutura mais robusta - Busca de dados: use
async/awaitdiretamente, semgetServerSideProps
O App Router é, de fato, o caminho futuro do Next.js. A Vercel continua investindo nele e a comunidade acompanha essa evolução. Se você está começando a aprender Next.js agora, vale começar diretamente pelo App Router.
E qual é o próximo passo?
Coloque a mão no código. Crie um projeto pequeno e tente montar um blog ou uma lista de tarefas com o App Router. Ler os conceitos ajuda, mas implementá-los uma vez esclarece muito mais.
Se encontrar um problema, não se desespere: Pages Router e App Router podem coexistir. Se for necessário, mantenha uma parte no Pages Router e migre aos poucos.
A documentação oficial do Next.js pode parecer confusa no primeiro contato, mas a seção do App Router é detalhada. Quando surgir uma dúvida específica, consulte a documentação ou procure discussões relacionadas no GitHub Discussions.
Bom aprendizado!
FAQ
Quando devo usar 'use client'?
Qual é a relação entre layout.js e page.js?
Como acessar parâmetros de uma rota dinâmica?
Por que error.js não captura erros de layout.js?
Preciso migrar um projeto antigo para o App Router?
Quais são as principais diferenças entre App Router e Pages Router?
13 min de leitura · Publicado em: 18 dez 2025 · Atualizado em: 4 set 2026
Guia completo Next.js
Você está lendo o primeiro post desta série. Continue para o próximo ou abra o hub da série para ver toda a trilha.



Comentários
Entre com GitHub para comentar