Alternar tema

Rotas avançadas no Next.js: guia completo de grupos, layouts aninhados, rotas paralelas e interceptação

Easton editorial illustration: server-client bridge

Na semana passada, assumi um projeto de e-commerce em Next.js que já estava em produção havia dois anos. Assim que abri o diretório app, deparei com mais de 60 pastas espremidas na tela: about, products, admin-users, marketing-campaign, shop-cart… tudo no mesmo nível. Para encontrar uma página relacionada a usuários, era preciso vasculhar uma pilha de páginas de marketing. Pior ainda: três pessoas da equipe alteravam os arquivos de rotas ao mesmo tempo, surgiam pelo menos dois conflitos no Git por dia e, durante o code review, levávamos meia hora só para entender a relação entre os arquivos.

Sentado diante daquela coleção de pastas, lembrei de repente: o Next.js não tem grupos de rotas, rotas paralelas e outros recursos avançados? Consultei a documentação oficial e descobri que esses recursos já existiam desde o Next.js 13, mas o projeto não usava nenhum deles. Em outras palavras, as ferramentas estavam disponíveis; o problema era não saber quando usar cada uma.

Naquela noite, passei três horas estudando esses recursos e mais dois dias refatorando a estrutura do projeto. O resultado? Os diretórios ficaram muito mais claros, e ninguém voltou a reclamar no grupo da equipe que havia “mais um conflito”. O mais importante foi finalmente entender os problemas resolvidos por quatro recursos que, à primeira vista, parecem complicados: grupos de rotas organizam os diretórios, layouts aninhados permitem reutilizar estruturas com flexibilidade, rotas paralelas exibem várias páginas ao mesmo tempo e rotas interceptadas implementam modais de forma elegante.

Se o seu projeto Next.js também cresceu, acumulou arquivos desorganizados e vive causando conflitos entre a equipe, ao terminar este artigo você saberá quando usar cada recurso, como implementá-lo e quais armadilhas evitar.

Grupos de rotas (Route Groups): diretórios bem organizados

O que são grupos de rotas?

Primeiro, a resposta direta: um grupo de rotas é uma pasta cujo nome fica entre parênteses, como (marketing) ou (shop). O detalhe especial é que o Next.js não inclui esse nome entre parênteses na URL.

Parece pouco útil? É porque você ainda não viu um caso real.

Imagine um e-commerce com páginas de marketing, como início e sobre nós; páginas da loja, como lista de produtos e carrinho; e um painel administrativo, com gestão de pedidos e usuários. Na abordagem tradicional, ou todas essas páginas ficam amontoadas na raiz de app, ou você força prefixos como /marketing, /shop e /admin na URL. Mas quem gostaria de mandar o usuário para um endereço estranho como yoursite.com/marketing/about?

Os grupos de rotas resolvem exatamente esse conflito: organizam as páginas por categoria no sistema de arquivos sem alterar as URLs vistas pelos usuários.

Três usos principais, todos muito práticos

Uso 1: separar diretórios por equipe ou função

Esse é o benefício mais direto. As 60 pastas antes no mesmo nível viram três grupos:

app/
├── (marketing)/    # Responsabilidade da equipe de marketing
│   ├── page.js     # Página inicial → yoursite.com/
│   ├── about/      # Sobre → yoursite.com/about
│   └── pricing/    # Preços → yoursite.com/pricing
├── (shop)/         # Responsabilidade da equipe de frontend
│   ├── products/   # Produtos → yoursite.com/products
│   └── cart/       # Carrinho → yoursite.com/cart
└── (dashboard)/    # Responsabilidade da equipe de backend
    ├── orders/     # Pedidos → yoursite.com/orders
    └── users/      # Usuários → yoursite.com/users

Percebeu? As URLs continuam limpas, mas a estrutura de arquivos fica evidente. Uma pessoa recém-chegada à equipe entende de imediato a finalidade de cada diretório.

Uso 2: layouts raiz diferentes para áreas diferentes

Essa é a maior vantagem dos grupos de rotas. As páginas de marketing e o painel administrativo deveriam ter a mesma navegação? Claro que não. Mas ambas começam em caminhos raiz, como /about e /orders. Como aplicar layouts diferentes?

A resposta é simples: cada grupo de rotas pode ter seu próprio layout.js.

app/
├── (marketing)/
│   ├── layout.js        # Layout de marketing: navegação superior + imagem de destaque
│   └── ...
├── (shop)/
│   ├── layout.js        # Layout da loja: ícone do carrinho + filtro por categorias
│   └── ...
└── (dashboard)/
    ├── layout.js        # Layout administrativo: barra lateral + verificação de permissões
    └── ...

São três layouts independentes. A área de marketing pode ter uma imagem de destaque chamativa, o painel pode usar uma sidebar, e a loja pode exibir permanentemente a quantidade de itens no carrinho. Tudo dentro do mesmo projeto, sem subdomínios nem várias instâncias do Next.js.

Uso 3: compartilhar layouts de forma seletiva

Às vezes, você quer que algumas páginas compartilhem um layout e outras não. Por exemplo, artigos de um blog podem precisar de um sumário à esquerda, enquanto a página inicial do blog não. Um grupo de rotas resolve isso com facilidade:

app/
├── blog/
│   ├── page.js         # Página inicial do blog, sem navegação lateral
│   └── (articles)/     # Grupo de artigos, com navegação lateral compartilhada
│       ├── layout.js   # Layout com navegação lateral
│       ├── [slug]/     # Detalhe do artigo → /blog/xxx
│       └── ...

Observe que o grupo (articles) não altera a URL: o caminho continua sendo /blog/my-first-post, mas somente as páginas desse grupo recebem o layout com navegação.

Caso real de refatoração: do caos à clareza

Voltemos ao projeto de e-commerce com 60 pastas. Veja a comparação antes e depois da refatoração.

Antes da refatoração (alguns arquivos):

app/
├── page.js
├── about/
├── pricing/
├── products/
├── products-detail/
├── cart/
├── checkout/
├── admin-orders/
├── admin-users/
├── admin-settings/
├── marketing-campaign/
├── ... (mais 50)

Para encontrar um arquivo, restava usar Ctrl+F. Para saber a qual módulo uma página pertencia, era preciso adivinhar pelo nome.

Depois da refatoração:

app/
├── (marketing)/
│   ├── layout.js
│   ├── page.js
│   ├── about/
│   ├── pricing/
│   └── campaign/
├── (shop)/
│   ├── layout.js
│   ├── products/
│   ├── cart/
│   └── checkout/
└── (dashboard)/
    ├── layout.js
    ├── orders/
    ├── users/
    └── settings/

Uma estrutura de três níveis, simples e clara. Quer alterar uma página de marketing? Vá direto a (marketing). Quer adicionar uma função administrativa? Crie o arquivo em (dashboard). Durante o code review, cada equipe passou a analisar apenas o próprio grupo, e a taxa de conflitos caiu 70%.

Três armadilhas que você precisa evitar

Armadilha 1: URLs conflitantes causam erro imediatamente

Como o grupo não aparece na URL, o que acontece se dois grupos tiverem uma rota com o mesmo nome?

app/
├── (marketing)/
│   └── about/page.js   # → /about
└── (shop)/
    └── about/page.js   # → /about (conflito!)

O Next.js exibe o erro Error: Conflicting route. A solução é simples: mude o nome do caminho ou adicione um nível real de diretório, sem parênteses:

app/
├── (marketing)/
│   └── about/page.js      # → /about
└── (shop)/
    └── shop-info/page.js  # → /shop-info (renomeado)

Armadilha 2: vários layouts raiz provocam uma atualização completa da página

Ao navegar de (shop) para (marketing), você perceberá uma pequena “piscada” na página. Não é bug, e sim o comportamento esperado.

Os layouts raiz de grupos diferentes são totalmente independentes. Na troca, o Next.js precisa desmontar o layout anterior e montar o novo, o que exige um carregamento completo da página, ou full page load. Isso é intencional, pois as duas áreas podem ter layouts completamente incompatíveis.

Se você quiser uma transição suave, evite vários layouts raiz: mova as partes compartilhadas para o app/layout.js mais externo e deixe nos layouts dos grupos apenas as diferenças.

Armadilha 3: posição da página inicial com vários layouts raiz

Se você criou vários grupos e cada um tem seu próprio layout.js, o page.js da página inicial precisa ficar dentro de um deles, e não em app/page.js. Caso contrário, o Next.js não saberá qual layout raiz usar.

Em geral, a página inicial fica em (marketing)/page.js, porque normalmente faz parte do conteúdo de marketing.


Em resumo, os grupos de rotas servem para organizar arquivos sem alterar a URL e ainda permitir layouts diferentes para cada área. Projetos pequenos talvez não precisem deles, mas, se o diretório app já tem mais de 20 pastas, é hora de considerar esse recurso.

Layouts aninhados (Nested Layouts): reutilização flexível da estrutura da página

Que problema os layouts aninhados resolvem?

Você já encontrou uma situação assim? A página inicial precisa de uma navegação superior; a lista do blog precisa da navegação superior mais um filtro de categorias à esquerda; e o detalhe do artigo precisa da navegação superior, do filtro à esquerda e de um sumário à direita.

Com uma composição tradicional de componentes, seria necessário montar tudo manualmente em cada página. Mudou o estilo da navegação? Três lugares precisam ser atualizados.

Os layouts aninhados do Next.js resolvem esse problema: os layouts se encaixam como bonecas russas, e o layout externo é aplicado automaticamente a todas as páginas internas. A cada nível mais profundo, você adiciona novos elementos de interface sobre o layout externo sem repetir o que já existe.

Como funcionam os layouts aninhados?

O conceito é simples: cada pasta pode ter seu próprio layout.js. Uma pasta filha herda automaticamente o layout da pasta pai e aplica mais uma camada por cima.

Veja um exemplo real. Imagine que você esteja criando uma plataforma de educação online:

app/
├── layout.js              # Layout raiz: navegação superior + Footer
└── courses/
    ├── layout.js          # Layout de cursos: layout raiz + categorias à esquerda
    ├── page.js            # Lista de cursos
    └── [id]/
        ├── layout.js      # Detalhe do curso: layout de cursos + progresso à direita
        └── page.js        # Um curso específico

Quando o usuário acessa /courses/123, a ordem de renderização é esta:

  1. Camada mais externa: app/layout.js envolve tudo, com navegação superior + Footer
  2. Camada intermediária: courses/layout.js fica dentro dela, com categorias à esquerda
  3. Camada mais interna: courses/[id]/layout.js adiciona mais uma camada, com progresso à direita
  4. Conteúdo da página: courses/[id]/page.js fica no centro de tudo

É como uma boneca russa, uma camada dentro da outra. Quer mudar a navegação superior? Basta alterar app/layout.js, e todas as páginas recebem a mudança.

Caso prático: três níveis de layout em um blog

Um exemplo direto ajuda mais. Em um blog técnico que criei, os requisitos eram estes:

  • Todas as páginas: navegação superior com início, sobre e contato + rodapé
  • Páginas do blog: navegação superior + filtro de categorias à esquerda
  • Detalhes do artigo: navegação superior + categorias à esquerda + sumário à direita

Com layouts aninhados, a implementação fica muito confortável:

app/
├── layout.js                    # Primeira camada: layout comum do site
│   └── <Header /><Footer />
└── blog/
    ├── layout.js                # Segunda camada: layout do blog
    │   └── <Sidebar />
    ├── page.js                  # Lista do blog, herda as duas primeiras camadas
    └── [slug]/
        ├── layout.js            # Terceira camada: layout do artigo
        │   └── <TableOfContents />
        └── page.js              # Detalhe do artigo, herda as três camadas

O código fica assim, em uma versão simplificada:

app/layout.js (primeira camada)

export default function RootLayout({ children }) {
  return (
    <html>
      <body>
        <Header />
        {children}   {/* Aqui será renderizado o layout filho ou a página */}
        <Footer />
      </body>
    </html>
  )
}

app/blog/layout.js (segunda camada)

export default function BlogLayout({ children }) {
  return (
    <div className="blog-container">
      <Sidebar />
      <main>{children}</main>  {/* Aqui será renderizado o conteúdo mais interno */}
    </div>
  )
}

app/blog/[slug]/layout.js (terceira camada)

export default function ArticleLayout({ children }) {
  return (
    <div className="article-container">
      {children}
      <TableOfContents />  {/* Sumário à direita */}
    </div>
  )
}

Percebeu? Cada layout se preocupa apenas com os elementos de interface que adiciona. Ele não precisa conhecer o Header nem a Sidebar externos, pois o Next.js organiza automaticamente o aninhamento.

Combinando com grupos de rotas

É aqui que os recursos ficam realmente poderosos. Os grupos de rotas fazem uma separação “horizontal” entre marketing, loja e painel, enquanto os layouts aninhados fazem uma composição “vertical”, adicionando camadas de interface.

Voltando ao exemplo do e-commerce, a área da loja pode ser projetada assim:

app/
└── (shop)/
    ├── layout.js             # Layout raiz da loja: carrinho + categorias no topo
    ├── products/
    │   ├── layout.js         # Lista de produtos: + filtros à esquerda
    │   ├── page.js           # Lista
    │   └── [id]/
    │       ├── layout.js     # Detalhe do produto: + breadcrumbs
    │       └── page.js       # Detalhe
    └── cart/
        └── page.js           # Carrinho, herda apenas o layout raiz da loja
  • Lista de produtos /products: layout raiz da loja + filtros
  • Detalhe do produto /products/123: layout raiz da loja + filtros + breadcrumbs, com as três camadas
  • Carrinho /cart: apenas o layout raiz da loja, pois não há outro layout.js no caminho

É flexível: os elementos de interface de cada página dependem totalmente da hierarquia de diretórios, sem uma série de condições como “se for a página de detalhes, mostre os breadcrumbs”.

Dois detalhes importantes

Detalhe 1: o layout não é renderizado novamente, o que favorece o desempenho

Ao navegar de /blog para /blog/my-post, app/layout.js e app/blog/layout.js não são renderizados novamente. O estado e a posição de rolagem deles permanecem; apenas o page.js mais interno é recarregado.

Isso significa que você pode manter no layout o estado de rolagem de uma barra lateral ou o texto digitado em uma busca. Esses valores não se perdem quando o usuário navega entre páginas internas, deixando a experiência muito fluida.

Detalhe 2: o layout não acessa os parâmetros da rota filha

Imagine uma rota dinâmica em app/products/[id]/page.js. O parâmetro [id] só pode ser obtido por params no page.js; o layout.js não o recebe diretamente.

Se o layout precisar renderizar conteúdo diferente de acordo com [id], como o título do produto, você terá de passar a informação por outro meio, por exemplo, com Context ou elevando os dados para uma camada superior.


A ideia central dos layouts aninhados é fazer a organização do código acompanhar a hierarquia visual da interface. Se a página tem várias camadas de fora para dentro, os diretórios também terão várias camadas de layout. Não é necessário copiar componentes por toda parte, e uma alteração em um único layout afeta todas as páginas da área.

Rotas paralelas (Parallel Routes): várias páginas ao mesmo tempo

Quando usar rotas paralelas?

Comecemos com um cenário real. Você precisa criar um dashboard administrativo com três módulos na mesma tela:

  • No canto superior esquerdo: gráfico de vendas
  • No canto superior direito: lista de pedidos recentes
  • Na parte inferior: alertas de estoque

Os dados desses módulos são independentes e carregam em velocidades diferentes. O gráfico pode levar algum tempo para ser calculado; os pedidos podem aparecer imediatamente; e o estoque vem de outra API.

Na abordagem tradicional, seria necessário buscar os três conjuntos de dados e renderizar os três componentes em dashboard/page.js. Se um módulo falhar, a página inteira pode parar. Para criar estados de carregamento individuais, você teria de administrar vários loading states manualmente.

As rotas paralelas resolvem isso permitindo renderizar vários “fragmentos de página” independentes na mesma tela, chamados oficialmente de slots. Cada slot pode ter seu próprio estado de carregamento, tratamento de erros e até uma navegação independente.

Sintaxe das rotas paralelas: slots definidos com @

A sintaxe central é única: uma pasta chamada @folder se transforma em um slot.

Veja o exemplo do dashboard:

app/
└── dashboard/
    ├── layout.js          # Recebe os três slots como props
    ├── @sales/            # Slot 1: estatísticas de vendas
    │   └── page.js
    ├── @orders/           # Slot 2: lista de pedidos
    │   └── page.js
    ├── @inventory/        # Slot 3: alertas de estoque
    │   └── page.js
    └── page.js            # Conteúdo principal, opcional

Observe os nomes @sales, @orders e @inventory: pastas iniciadas por @ são slots de rotas paralelas.

Em layout.js, o Next.js fornece esses slots como props:

export default function DashboardLayout({
  children,    // Conteúdo de page.js
  sales,       // Conteúdo de @sales/page.js
  orders,      // Conteúdo de @orders/page.js
  inventory    // Conteúdo de @inventory/page.js
}) {
  return (
    <div className="dashboard">
      <div className="widgets">
        <div className="widget">{sales}</div>
        <div className="widget">{orders}</div>
      </div>
      <div className="main">{children}</div>
      <div className="alerts">{inventory}</div>
    </div>
  )
}

Os três slots funcionam como “subpáginas” independentes, e você pode distribuí-los livremente pelo layout. Em vez de concentrar toda a lógica em um grande page.js, cada módulo ganha seu próprio arquivo.

Caso prático: dashboard administrativo

Veja uma implementação concreta. Primeiro, o conteúdo dos três slots:

@sales/page.js (estatísticas de vendas)

async function getSalesData() {
  const res = await fetch('https://api.example.com/sales')
  return res.json()
}

export default async function SalesWidget() {
  const data = await getSalesData()  // Pode ser lento
  return (
    <div>
      <h3>Vendas do mês</h3>
      <Chart data={data} />
    </div>
  )
}

@orders/page.js (lista de pedidos)

async function getRecentOrders() {
  const res = await fetch('https://api.example.com/orders')
  return res.json()
}

export default async function OrdersWidget() {
  const orders = await getRecentOrders()  // Pode ser rápido
  return (
    <div>
      <h3>Pedidos recentes</h3>
      <ul>
        {orders.map(order => <li key={order.id}>{order.title}</li>)}
      </ul>
    </div>
  )
}

@inventory/page.js (alertas de estoque)

export default function InventoryWidget() {
  // Também pode ser um Client Component que busca dados com useEffect
  return (
    <div>
      <h3>Alertas de estoque</h3>
      <p>5 produtos estão com estoque baixo</p>
    </div>
  )
}

É aqui que acontece algo importante: os três slots carregam em paralelo. Se a API de pedidos for rápida, a lista aparece primeiro; se o cálculo de vendas for lento, ele termina depois. Se um slot gerar erro, apenas aquele slot falha, enquanto os outros continuam visíveis.

Isso é muito mais elegante do que a abordagem tradicional: você não precisa gerenciar três loading states, não espera pela interface mais lenta e cada módulo fica naturalmente isolado.

Estados independentes de carregamento e erro

Melhor ainda, cada slot pode ter seus próprios loading.js e error.js:

app/
└── dashboard/
    ├── @sales/
    │   ├── page.js
    │   ├── loading.js     # Estado de carregamento do módulo de vendas
    │   └── error.js       # Tratamento de erro do módulo de vendas
    ├── @orders/
    │   ├── page.js
    │   └── loading.js     # Estado de carregamento do módulo de pedidos
    └── @inventory/
        └── page.js

@sales/loading.js:

export default function SalesLoading() {
  return <div>Carregando dados de vendas...</div>
}

@sales/error.js:

'use client'

export default function SalesError({ error, reset }) {
  return (
    <div>
      <p>Não foi possível carregar os dados de vendas</p>
      <button onClick={reset}>Tentar novamente</button>
    </div>
  )
}

Agora, quando a página abre, o módulo de vendas mostra “Carregando…”, enquanto os pedidos talvez já tenham sido renderizados e o estoque apareça imediatamente. Se a API de vendas falhar, apenas aquele módulo mostra a mensagem de erro; os demais não são afetados.

A experiência do usuário melhora muito: não é necessário encarar uma tela vazia enquanto todos os dados carregam, e o conteúdo disponível já pode ser consultado.

Ponto essencial: a função de default.js

Há um detalhe importante. Imagine que você esteja em /dashboard e clique em um link para /dashboard/settings. As rotas dos slots @sales e @orders não correspondem a /dashboard/settings. O que o Next.js deve renderizar?

O comportamento padrão é manter o conteúdo anterior do slot, de modo parecido com uma atualização parcial em uma SPA. Se você quiser que o slot desapareça ao trocar de rota, use default.js:

app/
└── dashboard/
    ├── @sales/
    │   ├── page.js
    │   └── default.js     # Retorna null quando não houver correspondência
    └── ...

@sales/default.js:

export default function SalesDefault() {
  return null  // Não exibe nada quando não houver correspondência
}

Assim, quando a rota muda para /dashboard/settings, o slot @sales renderiza o conteúdo de default.js, isto é, nada.

O uso mais comum das rotas paralelas é, na verdade, em conjunto com rotas interceptadas para criar modais. Veremos isso em detalhes na próxima seção. Mas elas também funcionam muito bem em dashboards formados por vários módulos independentes.

Rotas interceptadas (Intercepting Routes): uma implementação elegante de modais

A experiência de visualizar imagens no Instagram

Você certamente já usou o Instagram ou outro aplicativo com uma experiência parecida. Ao clicar em uma imagem no feed, ela abre em um modal, enquanto a barra de endereço muda para /photo/abc123. E acontece algo interessante:

  • Ao usar o botão Voltar do navegador, o modal fecha e você retorna ao feed, em vez de ir para uma página totalmente diferente
  • Ao atualizar, o modal desaparece e a página completa da imagem é exibida
  • Ao compartilhar a URL, outra pessoa abre a página completa, não o modal

Como implementar isso? A abordagem tradicional exige bastante gerenciamento de estado, análise da URL e manipulação do histórico.

As rotas interceptadas foram criadas para esse cenário: elas conseguem interceptar a navegação no cliente e exibir o conteúdo da rota de destino em um modal sobre a página atual; porém, ao acessar diretamente a URL ou atualizar, renderizam a página completa.

Sintaxe das rotas interceptadas: os símbolos com pontos

A sintaxe usa parênteses e pontos para indicar o nível da rota a interceptar:

  • (.) — intercepta uma rota no mesmo nível
  • (..) — intercepta uma rota um nível acima
  • (..)(..) — intercepta uma rota dois níveis acima
  • (...) — intercepta uma rota a partir da raiz

Parece abstrato? Um exemplo esclarece.

Imagine uma lista de fotos em /photos. Ao clicar em uma foto, a navegação vai para /photos/123. Você quer que:

  • Ao clicar no link: um modal apareça sobre a lista
  • Ao acessar diretamente ou atualizar: a página completa da foto seja exibida

A estrutura de diretórios fica assim:

app/
├── @modal/
│   ├── (.)photos/        # Intercepta a rota photos no mesmo nível
│   │   └── [id]/
│   │       └── page.js   # Conteúdo do modal
│   └── default.js        # Retorna null quando não houver correspondência
├── layout.js             # Recebe o slot modal
├── page.js               # Feed da página inicial
└── photos/
    └── [id]/
        └── page.js       # Página completa da foto

Observe @modal/(.)photos/. Isso significa “intercepte a rota photos que está no mesmo nível de @modal”. Como @modal e photos ficam diretamente em app/, usa-se (.).

Caso prático: modal de imagens no estilo do Instagram

Vamos implementar tudo. Os requisitos são claros:

  • A página inicial exibe uma grade de imagens
  • Ao clicar em uma imagem, abre um modal e a URL muda para /photos/123
  • Ao atualizar ou acessar /photos/123 diretamente, aparece uma página completa
  • O botão Voltar fecha o modal

Etapa 1: estrutura de diretórios

app/
├── @modal/
│   ├── (.)photos/
│   │   └── [id]/
│   │       └── page.js   # Componente do modal
│   └── default.js
├── layout.js
├── page.js               # Grade de imagens na página inicial
└── photos/
    └── [id]/
        └── page.js       # Página completa da imagem

Etapa 2: o layout raiz recebe o slot modal

app/layout.js:

export default function RootLayout({ children, modal }) {
  return (
    <html>
      <body>
        {children}  {/* Área do conteúdo principal */}
        {modal}     {/* Slot do modal */}
      </body>
    </html>
  )
}

Etapa 3: a página inicial exibe a grade de imagens

app/page.js:

import Link from 'next/link'

const photos = [
  { id: '1', url: '/images/photo1.jpg' },
  { id: '2', url: '/images/photo2.jpg' },
  // ...
]

export default function HomePage() {
  return (
    <div className="photo-grid">
      {photos.map(photo => (
        <Link key={photo.id} href={`/photos/${photo.id}`}>
          <img src={photo.url} alt="" />
        </Link>
      ))}
    </div>
  )
}

Etapa 4: rota interceptada, o componente do modal

app/@modal/(.)photos/[id]/page.js:

'use client'

import { useRouter } from 'next/navigation'
import Image from 'next/image'

export default function PhotoModal({ params }) {
  const router = useRouter()

  return (
    <div className="modal-backdrop" onClick={() => router.back()}>
      <div className="modal-content" onClick={e => e.stopPropagation()}>
        <button onClick={() => router.back()}>Fechar</button>
        <Image src={`/images/photo${params.id}.jpg`} fill />
      </div>
    </div>
  )
}

Observe o uso de router.back() para fechar o modal. Como a tela foi aberta por uma navegação no cliente, voltar retorna ao feed.

Etapa 5: página completa

app/photos/[id]/page.js:

import Image from 'next/image'

export default function PhotoPage({ params }) {
  return (
    <div className="photo-page">
      <nav>Voltar para a página inicial</nav>
      <h1>Detalhes da imagem</h1>
      <Image src={`/images/photo${params.id}.jpg`} width={800} height={600} />
      <p>Descrição da imagem...</p>
    </div>
  )
}

Etapa 6: default.js garante que o modal possa ser fechado

app/@modal/default.js:

export default function Default() {
  return null  // O modal não aparece quando a rota não corresponde
}

O momento da mágica: o resultado na prática

Agora acontece o seguinte:

  1. Ao clicar em uma imagem na página inicial:

    • O componente Link inicia a navegação no cliente para /photos/1
    • O Next.js identifica que @modal/(.)photos/[id] corresponde ao destino e intercepta a navegação
    • O componente do modal é renderizado sobre o feed
    • A URL muda para /photos/1, mas a página não é recarregada por completo
  2. Ao usar o botão Voltar:

    • router.back() retorna à rota anterior, a página inicial /
    • O slot @modal deixa de corresponder e renderiza default.js, que retorna null
    • O modal desaparece, e o feed permanece como estava
  3. Ao atualizar a página ou acessar /photos/1 diretamente:

    • Como não é uma navegação no cliente, o Next.js não faz a interceptação
    • A página completa em app/photos/[id]/page.js é renderizada diretamente
    • Não há modal; o usuário vê uma página independente de detalhes da imagem
  4. Ao compartilhar o link:

    • Quem abre /photos/1 vê a página completa
    • A experiência é igual à de um acesso direto

Perfeito: a URL pode ser compartilhada, o botão Voltar fecha o modal e uma atualização mostra a página completa. Todos os três requisitos foram atendidos.

Como escolher o nível da interceptação?

Já vimos as sintaxes (.), (..) e (...), mas qual usar? O que importa é a posição relativa entre a rota de interceptação e a rota de destino.

Se a rota interceptada fica em app/@modal/:

  • A rota de destino é app/photos/, no mesmo nível → use (.)photos
  • A rota de destino é app/shop/products/, em uma rota filha do nível acima → use (..)
  • A rota de destino pode estar em qualquer lugar, inclusive muito aninhada → use (...) para interceptar desde a raiz

Por exemplo, considere esta estrutura:

app/
└── shop/
    ├── @modal/
    │   └── (..)products/   # Intercepta products a partir do nível acima
    │       └── [id]/
    └── products/
        └── [id]/

Aqui, @modal fica dentro de shop/ e deve interceptar shop/products/. Usa-se (..) porque products está no diretório pai shop.

No começo, essa relação realmente pode confundir. Minha sugestão é iniciar com (...) para interceptar desde a raiz. Depois que funcionar, ajuste para (.) ou (..) de acordo com a estrutura real.

Três armadilhas importantes

Armadilha 1: esquecer default.js impede o fechamento do modal

Se @modal não tiver default.js, o slot pode manter o conteúdo anterior quando a rota não corresponder mais, deixando o modal aberto. É obrigatório criar um default.js que retorne null.

Armadilha 2: usar useRouter em um Server Component

O modal de uma rota interceptada normalmente precisa de useRouter().back() para ser fechado, o que exige um Client Component. Não se esqueça da diretiva 'use client'.

Armadilha 3: a interceptação falha em estruturas muito aninhadas

Se a estrutura de diretórios for profunda, como app/shop/(store)/products/[id], calcule o caminho de interceptação com cuidado. Se não conseguir determinar a relação, use (...) para interceptar desde a raiz. Pode ser menos refinado, mas funciona.


A combinação de rotas interceptadas e paralelas resolve todos os desafios de um modal: URL compartilhável, página completa ao atualizar, fechamento ao voltar e reabertura ao avançar. Instagram, Twitter e Airbnb oferecem esse tipo de experiência, e agora você também pode implementá-la no Next.js.

Exemplo completo: combinando as quatro técnicas

Cenário real: a estrutura completa de uma plataforma de e-commerce

Nas quatro seções anteriores, vimos cada recurso isoladamente. Eles já são úteis assim, mas sua verdadeira força aparece quando são combinados. Vamos partir dos requisitos de uma plataforma real de e-commerce e aplicar grupos de rotas, layouts aninhados, rotas paralelas e rotas interceptadas ao mesmo tempo.

Análise dos requisitos

Imagine uma plataforma de e-commerce de médio porte com estes requisitos:

Três grandes áreas funcionais, cada uma com seu próprio layout:

  • Área de marketing (/, /about, /pricing): imagem de destaque + navegação simples
  • Área da loja (/products, /cart): ícone fixo do carrinho + navegação por categorias
  • Área administrativa (/dashboard): barra lateral + verificação de permissões

Requisitos específicos da loja:

  • A lista de produtos precisa de filtros à esquerda
  • A página de detalhes precisa de breadcrumbs
  • Ao clicar em um card de produto, deve abrir um modal de visualização rápida sem sair da lista
  • Ao atualizar ou acessar diretamente a URL de detalhes, deve aparecer a página completa

Requisitos do dashboard administrativo:

  • Exibir ao mesmo tempo estatísticas de vendas, lista de pedidos e alertas de estoque em três módulos independentes
  • Cada módulo precisa de seu próprio estado de carregamento e tratamento de erros

Estrutura completa dos diretórios

Antes do código, observe a estrutura geral e o recurso usado em cada nível:

app/
├── layout.js                          # Layout raiz global

├── (marketing)/                       # Grupo de rotas: marketing
│   ├── layout.js                      # Layout de marketing
│   ├── page.js                        # Página inicial → /
│   ├── about/                         # Sobre → /about
│   └── pricing/                       # Preços → /pricing

├── (shop)/                            # Grupo de rotas: loja
│   ├── layout.js                      # Layout da loja
│   ├── @modal/                        # Rota paralela: slot do modal
│   │   ├── (.)products/               # Rota interceptada: visualização rápida
│   │   │   └── [id]/
│   │   │       └── page.js            # Componente do modal
│   │   └── default.js
│   │
│   ├── products/
│   │   ├── layout.js                  # Layout aninhado: filtros de produtos
│   │   ├── page.js                    # Lista → /products
│   │   └── [id]/
│   │       ├── layout.js              # Layout aninhado: breadcrumbs
│   │       └── page.js                # Detalhe → /products/123
│   │
│   └── cart/
│       └── page.js                    # Carrinho → /cart

└── (dashboard)/                       # Grupo de rotas: área administrativa
    ├── layout.js                      # Layout administrativo, com barra lateral
    ├── @sales/                        # Rota paralela: estatísticas de vendas
    │   ├── page.js
    │   └── loading.js
    ├── @orders/                       # Rota paralela: lista de pedidos
    │   ├── page.js
    │   └── loading.js
    ├── @inventory/                    # Rota paralela: alertas de estoque
    │   └── page.js
    └── page.js                        # Página principal → /dashboard

Percebeu? Nessa estrutura:

  • Grupos de rotas separam as três áreas principais
  • Layouts aninhados adicionam progressivamente elementos de interface na loja
  • Rotas paralelas criam o slot do modal na loja e os vários módulos do dashboard
  • Rotas interceptadas implementam a visualização rápida do produto

Implementação do código principal

Não vou reproduzir todo o código, pois ficaria longo demais. Vamos destacar os pontos principais.

1. O layout da loja recebe o slot do modal

app/(shop)/layout.js:

export default function ShopLayout({ children, modal }) {
  return (
    <div>
      <nav>{/* Ícone do carrinho + navegação por categorias */}</nav>
      {children}
      {modal}  {/* O modal será sobreposto aqui */}
    </div>
  )
}

2. Layout aninhado da lista de produtos

app/(shop)/products/layout.js:

export default function ProductsLayout({ children }) {
  return (
    <div className="products-container">
      <aside>{/* Filtros à esquerda */}</aside>
      <main>{children}</main>
    </div>
  )
}

app/(shop)/products/page.js (lista):

import Link from 'next/link'

export default function ProductsPage() {
  return (
    <div className="product-grid">
      {products.map(p => (
        <Link key={p.id} href={`/products/${p.id}`}>
          <ProductCard product={p} />
        </Link>
      ))}
    </div>
  )
}

Ao clicar em um card, ocorre uma navegação no cliente, a rota interceptada entra em ação e o modal aparece.

3. Rota interceptada para a visualização rápida

app/(shop)/@modal/(.)products/[id]/page.js:

'use client'

import { useRouter } from 'next/navigation'

export default function ProductModal({ params }) {
  const router = useRouter()

  return (
    <div className="modal-backdrop" onClick={() => router.back()}>
      <div className="modal">
        <h2>Visualização rápida do produto</h2>
        <ProductPreview id={params.id} />
        <Link href={`/products/${params.id}`} onClick={() => router.back()}>
          Ver detalhes completos
        </Link>
      </div>
    </div>
  )
}

4. Rotas paralelas do dashboard administrativo

app/(dashboard)/layout.js:

export default function DashboardLayout({ children, sales, orders, inventory }) {
  return (
    <div className="dashboard">
      <aside>{/* Navegação lateral */}</aside>
      <main>
        {children}
        <div className="widgets">
          <div className="widget">{sales}</div>
          <div className="widget">{orders}</div>
          <div className="widget">{inventory}</div>
        </div>
      </main>
    </div>
  )
}

Os três slots carregam em paralelo. Os rápidos aparecem primeiro; os lentos terminam no próprio ritmo; nenhum interfere no outro.

Motivos por trás das decisões de arquitetura

Por que projetar dessa forma? Cada decisão tem uma razão.

Por que usar grupos de rotas?

  • Marketing, loja e painel têm barras de navegação totalmente diferentes e precisam de layouts raiz distintos
  • Na colaboração, cada equipe cuida do próprio grupo e reduz os conflitos

Por que usar layouts aninhados?

  • A lista de produtos precisa de filtros e o detalhe precisa de breadcrumbs, mas ambos herdam a navegação superior da loja
  • Os layouts aninhados alinham a hierarquia da interface à dos diretórios e mantêm o código claro

Por que combinar rotas interceptadas e paralelas no modal?

  • O usuário quer visualizar rapidamente um produto sem sair da lista
  • A URL precisa mudar para /products/123, permitindo compartilhamento e SEO
  • Ao atualizar, a página completa deve aparecer; não pode existir apenas o modal

Por que usar rotas paralelas no painel?

  • Os três módulos usam fontes de dados e velocidades de carregamento diferentes
  • Cada um pode carregar e tratar erros de forma independente
  • A falha de um módulo não afeta os demais

Benefícios reais para a equipe

Depois dessa refatoração, a equipe relatou estes resultados:

  • 65% menos conflitos: o time de frontend altera (shop), enquanto o de backend altera (dashboard), sem interferência
  • Metade do tempo para se ambientar: uma pessoa nova entende pelo diretório qual arquivo controla cada função, sem precisar vasculhar o código
  • Menor custo de manutenção: para alterar uma navegação, basta mudar o layout.js do grupo correspondente, sem afetar outras áreas
  • Melhor experiência do usuário: a conversão da visualização rápida de produtos ficou 23% maior do que a navegação anterior para a página de detalhes, pois muitos usuários não queriam voltar manualmente

No fim, esses quatro recursos não existem para “exibir habilidade técnica”, e sim para deixar o código mais claro, facilitar a colaboração e tornar a experiência do usuário mais fluida. Um projeto pequeno não precisa de tanta estrutura, mas, se o seu já tem dezenas de rotas, várias equipes e interações complexas por modal, essa arquitetura pode fazer uma grande diferença.

Conclusão

Voltando ao projeto do início, com 60 pastas desorganizadas: depois da refatoração, a maior mudança não foi o nível de sofisticação técnica, e sim poder voltar a se concentrar na lógica do negócio, sem perder tempo procurando arquivos, resolvendo conflitos e mantendo layouts duplicados.

Aqui está um resumo rápido dos quatro recursos:

RecursoFunção principalQuando usarSintaxe principal
Grupos de rotasOrganizar arquivos e separar layoutsVárias áreas funcionais e equipes(folderName)
Layouts aninhadosAdicionar camadas sucessivas de interfaceNavegação em vários níveis e elementos progressivosUm layout.js em cada nível
Rotas paralelasRenderizar vários fragmentos de página ao mesmo tempoDashboards e módulos independentes@folderName
Rotas interceptadasInterceptar a navegação e abrir um modalModais no estilo do Instagram(.) (..) (...)

Minha recomendação é: não use tudo de uma vez. Comece com grupos de rotas para organizar diretórios confusos. Quando surgir uma interface em vários níveis, adote layouts aninhados. Considere rotas paralelas e interceptadas somente ao criar um dashboard ou um modal.

Para terminar, uma observação: esses recursos avançados realmente parecem confusos no começo. Na primeira vez que li a documentação oficial, também levei algum tempo para entender. Depois de usá-los uma vez, porém, sua lógica fica bastante natural: a estrutura dos diretórios define as rotas, a hierarquia dos arquivos define a interface e a lógica de interceptação define a experiência do usuário.

Se o seu projeto Next.js está ficando grande e desorganizado, vale reservar algumas horas para experimentar uma refatoração com grupos de rotas. Acredite: daqui a três meses, você agradecerá por ter começado hoje.

Processo completo de refatoração com rotas avançadas no Next.js

Como transformar uma estrutura de diretórios desorganizada em uma arquitetura clara com grupos de rotas, layouts aninhados, rotas paralelas e rotas interceptadas

⏱️ Estimated time: 4 hr

  1. 1

    Step 1: Analise a estrutura atual do projeto

    Avalie a quantidade e a complexidade das rotas do projeto:
    • Conte as pastas dentro do diretório app; se houver mais de 20, considere usar grupos de rotas
    • Identifique áreas funcionais diferentes, como marketing, loja e painel administrativo
    • Localize grupos de páginas que precisam de layouts diferentes
    • Registre os pontos de conflito na colaboração da equipe

    Critérios de decisão:
    • Mais de 20 pastas: use grupos de rotas
    • Navegação em vários níveis: use layouts aninhados
    • Vários módulos independentes na mesma tela: use rotas paralelas
    • Interação por modal: use rotas interceptadas com rotas paralelas
  2. 2

    Step 2: Crie grupos de rotas para separar áreas funcionais

    Use parênteses para criar grupos de rotas por função ou equipe:
    • Crie o grupo (marketing): páginas de marketing, como início, sobre e preços
    • Crie o grupo (shop): páginas da loja, como produtos e carrinho
    • Crie o grupo (dashboard): páginas administrativas, como pedidos e usuários

    Cuidados:
    • O nome do grupo não altera a URL, mas a mesma URL não pode existir em mais de um grupo
    • Cada grupo pode ter seu próprio layout.js
    • O page.js da página inicial precisa ficar dentro de um dos grupos, e não em app/page.js
  3. 3

    Step 3: Projete os níveis dos layouts aninhados

    Projete os layouts aninhados de acordo com a hierarquia da interface:
    • Primeiro nível: app/layout.js, com o layout comum do site, Header + Footer
    • Segundo nível: layout.js da área funcional, como blog/layout.js com a barra lateral
    • Terceiro nível: layout.js da página de detalhes, como blog/[slug]/layout.js com a navegação do sumário

    Pontos de implementação:
    • Cada layout adiciona apenas os elementos de interface específicos daquele nível
    • Layouts filhos herdam automaticamente os layouts pais
    • Layouts não são renderizados novamente, o que favorece o desempenho
  4. 4

    Step 4: Implemente rotas paralelas, se necessário

    Use o símbolo @ para criar slots:
    • Crie o slot @modal para modais
    • Crie os slots @sales e @orders para módulos independentes do dashboard

    Receba os slots em layout.js:
    • export default function Layout({ children, modal, sales, orders })
    • Renderize no JSX: {modal} {sales} {orders}

    Cada slot pode ter seus próprios loading.js e error.js
  5. 5

    Step 5: Implemente rotas interceptadas, se precisar de um modal

    Crie a estrutura da rota interceptada:
    • Em @modal, crie (.)photos/[id]/page.js para interceptar uma rota no mesmo nível
    • Crie photos/[id]/page.js para a página completa

    Sintaxe:
    • (.): intercepta uma rota no mesmo nível
    • (..): intercepta uma rota um nível acima
    • (...): intercepta a partir da raiz

    Crie obrigatoriamente um default.js que retorne null para permitir que o modal seja fechado
  6. 6

    Step 6: Teste e valide

    Valide todos os recursos:
    • Teste os grupos de rotas: confirme que a URL não mudou e a estrutura de arquivos ficou clara
    • Teste os layouts aninhados: confirme a hierarquia da interface e a preservação do estado
    • Teste as rotas paralelas: confirme o carregamento independente e o isolamento de erros
    • Teste as rotas interceptadas: confirme que o modal aparece na navegação pelo cliente e que uma atualização mostra a página completa

    Verificação de desempenho:
    • Use o Next.js DevTools para conferir quantas vezes os layouts são renderizados
    • Confirme que o layout não é renderizado novamente ao alternar entre rotas filhas

FAQ

Grupos de rotas alteram a URL?
Não. Os grupos usam nomes entre parênteses, como (marketing), e o Next.js ignora esse nome na URL. Por exemplo, a URL de (marketing)/about/page.js continua sendo /about, e não /marketing/about.
Quando devo usar grupos de rotas?
Use grupos de rotas quando o diretório app tiver mais de 20 pastas ou quando áreas funcionais diferentes precisarem de layouts raiz distintos. Eles são especialmente úteis em projetos com várias equipes e podem reduzir bastante os conflitos no Git.
Layouts aninhados prejudicam o desempenho?
Não. Os layouts aninhados do Next.js não são renderizados novamente durante a navegação entre rotas filhas; apenas o page.js mais interno é recarregado. Por isso, o layout pode preservar estados como a posição de rolagem da barra lateral ou o conteúdo de uma busca durante a troca de página.
Qual é a diferença entre uma rota paralela e um componente comum?
Cada slot de uma rota paralela é um fragmento de página independente, com seus próprios loading.js e error.js, e pode carregar sem bloquear os demais. Em uma composição de componentes comuns, normalmente é preciso aguardar todos os dados, e uma falha pode afetar a página inteira.
Como escolher entre (.), (..) e (...) nas rotas interceptadas?
Use (.) para interceptar uma rota no mesmo nível, como @modal e photos dentro de app; (..) para uma rota um nível acima; e (...) para interceptar a partir da raiz. Se estiver em dúvida, comece com (...) e, depois que funcionar, ajuste de acordo com a estrutura dos diretórios.
Por que uma rota interceptada precisa de default.js?
Sem default.js, quando a rota deixa de corresponder, o slot pode manter o conteúdo anterior e o modal continuar aberto. Crie um default.js que retorne null para que o modal seja fechado corretamente quando não houver correspondência.
É preciso refatorar as rotas ao migrar do Pages Router para o App Router?
Não necessariamente. Um projeto pequeno, com menos de 20 pastas, pode manter uma estrutura plana. Para projetos maiores ou que precisam de layouts e interações mais complexos, grupos de rotas e layouts aninhados podem melhorar bastante a manutenção do código.

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

Comentários

Entre com GitHub para comentar

Easton BlogEaston Blog