Alternar tema

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

Easton editorial illustration: one large folder tree unfolding into a route map

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/await diretamente 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:

  1. Coloque error.js no diretório pai
  2. Use global-error.js no 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:

  1. Roteamento pelo sistema de arquivos: a estrutura de pastas define as rotas, e page.js é o ponto de entrada
  2. Server Components: executados no servidor por padrão, com melhor desempenho
  3. Client Components: marcados com 'use client' quando há interatividade
  4. Arquivos especiais: layout.js, loading.js e error.js tornam a estrutura mais robusta
  5. Busca de dados: use async/await diretamente, sem getServerSideProps

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'?
Use apenas quando precisar de interatividade: React hooks (useState, useEffect), tratamento de ações do usuário (onClick, onChange) ou APIs do navegador (window, localStorage). Por padrão, um Server Component oferece melhor desempenho.
Qual é a relação entre layout.js e page.js?
O layout.js envolve o page.js e as rotas filhas. Durante a navegação, o layout.js não é renderizado novamente; apenas o page.js é atualizado. Assim, elementos compartilhados, como a barra de navegação, não piscam na tela.
Como acessar parâmetros de uma rota dinâmica?
Use a propriedade params do componente. Por exemplo, em app/blog/[slug]/page.js, receba { params } e acesse params.slug para obter o valor de slug presente na URL.
Por que error.js não captura erros de layout.js?
Essa é uma limitação dos React Error Boundaries: eles só capturam erros de componentes filhos, não de componentes no mesmo nível ou em níveis superiores. Para capturar erros de layout, coloque error.js no diretório pai ou use global-error.js no diretório raiz.
Preciso migrar um projeto antigo para o App Router?
Não é necessário migrar imediatamente. Pages Router e App Router podem coexistir: mantenha os recursos antigos em pages/ e crie os novos em app/. A Vercel afirma que o Pages Router continuará com suporte de longo prazo. Para projetos novos, porém, vale começar diretamente com o App Router.
Quais são as principais diferenças entre App Router e Pages Router?
O App Router se baseia em Server Components, renderiza no servidor por padrão e reduz o JavaScript enviado ao navegador. Ele usa roteamento pelo sistema de arquivos, oferece layouts aninhados e permite buscar dados diretamente com async/await. O Pages Router usa principalmente componentes de cliente e recorre a getServerSideProps para buscar dados no servidor.

13 min de leitura · Publicado em: 18 dez 2025 · Atualizado em: 4 set 2026

Comentários

Entre com GitHub para comentar

Easton BlogEaston Blog