Next.js App Router na prática: use route groups e layouts aninhados para organizar diretórios em projetos grandes

A árvore de arquivos do VS Code mostra mais de 120 pastas espremidas dentro do diretório app. Você quer encontrar a página de gerenciamento de usuários do painel administrativo e passa cinco minutos alternando entre dashboard-user-list, admin-users e backend-user-management, porque todas parecem a mesma coisa.
Na revisão de código da manhã, o Xiao Li adicionou uma rota /about, mas ela bateu com a /about que o time de marketing tinha enviado na semana anterior. Os dois ficaram se encarando: “O seu about é sobre nós, o meu about é sobre o produto. Por que eu tenho que mudar?”
Sendo sincero, essa não foi a primeira vez. No início do projeto, havia só uma dúzia de páginas, e uma estrutura plana parecia até limpa. Meio ano depois, as funcionalidades multiplicaram por dez, e o diretório app virou um armário que ninguém arrumou: você sabe que as coisas estão lá dentro, mas toda vez precisa revirar tudo.
Se você também trabalha com projetos Next.js, se sua equipe tem mais de três pessoas e se o número de páginas já passou de 50, há uma boa chance de você encontrar o mesmo problema. A boa notícia é que o Next.js App Router oferece quatro recursos feitos justamente para isso: route groups, layouts aninhados, parallel routes e intercepting routes. O problema é que muitos tutoriais na internet ficam no nível de demo “Hello World”; quando chega a hora de aplicar em um projeto real, ainda bate aquela névoa.
Neste artigo, vou usar o exemplo de um projeto real de e-commerce para mostrar como esses quatro recursos funcionam e como reorganizar um diretório bagunçado em uma estrutura sustentável e extensível.
Reproduzindo o problema - três grandes falhas da estrutura tradicional
A armadilha do diretório plano
Primeiro, veja como era o nosso diretório antes:
app/
├── page.tsx # Página inicial
├── about/page.tsx # Sobre nós
├── products/page.tsx # Lista de produtos
├── product-detail/[id]/page.tsx
├── cart/page.tsx
├── checkout/page.tsx
├── dashboard/page.tsx # Página inicial do painel
├── dashboard-users/page.tsx
├── dashboard-users-active/page.tsx
├── dashboard-users-blocked/page.tsx
├── dashboard-orders/page.tsx
├── dashboard-orders-pending/page.tsx
├── dashboard-settings/page.tsx
├── auth-login/page.tsx # Login
├── auth-register/page.tsx
└── ... (mais 80+ pastas)
Só de olhar já cansa. O pior é que as URLs também ficam estranhas: /dashboard-users-active em vez de /dashboard/users/active. Para evitar conflitos, fomos obrigados a adicionar vários prefixos aos nomes das pastas, mas isso só escondeu o problema.
Você simplesmente não consegue perceber de primeira quais páginas pertencem ao frontend público, quais pertencem ao painel interno e quais são relacionadas à autenticação. Quando uma pessoa nova entra na equipe, só entender a estrutura de diretórios já leva vários dias.
Layouts repetidos e manutenção difícil
O layout do frontend público e o do painel administrativo são completamente diferentes. O frontend tem navegação superior e rodapé com copyright; o painel tem sidebar e controle de permissões. A abordagem tradicional é importar manualmente o layout em cada componente de página:
// app/dashboard-users/page.tsx
import DashboardLayout from '@/components/DashboardLayout'
export default function UsersPage() {
return (
<DashboardLayout>
<div>Conteúdo de gerenciamento de usuários</div>
</DashboardLayout>
)
}
Esse jeito traz alguns problemas. Primeiro, é fácil esquecer: você cria uma nova página do painel, esquece de adicionar o layout, e a página fica nua. Segundo, fica inconsistente: uma pessoa usa DashboardLayout, outra usa AdminLayout, e no fim a manutenção vira uma bagunça.
O pior é que, se você precisar alterar o layout do painel, como ajustar o estilo da sidebar, talvez tenha que abrir 20 arquivos para confirmar se cada página usa o componente de layout correto. Sinceramente, toda vez que eu mexia no layout ficava tenso.
O dilema de rotas para modais e pop-ups
O gerente de produto trouxe um requisito: quando o usuário clicar em um produto na lista, abrir um modal com os detalhes e, ao mesmo tempo, mudar a URL para /product/123, para que o link possa ser compartilhado. Parece razoável, mas implementar isso do jeito tradicional dá dor de cabeça.
A abordagem clássica é usar estado no cliente, controlar manualmente a abertura e o fechamento do modal e depois manipular a URL à mão. O código fica desagradável, e ainda há um problema grave: se o usuário atualizar a página, o modal desaparece, e a experiência fica ruim.
Você talvez diga: então fazemos duas versões, uma para o modal e outra para a página completa. Dá para fazer, sim. Mas isso significa manter o mesmo conteúdo em dois lugares. Quando a lógica do produto muda, os dois lados precisam mudar, e é fácil introduzir bug.
A experiência parecida com a do Instagram, em que você clica em uma imagem na lista, abre um modal e, ao atualizar a página, vê a imagem em página completa, parece simples. Mas com roteamento tradicional, implementar isso é realmente trabalhoso.
Route Groups - agrupamento lógico sem afetar a URL
O que são route groups
Em termos simples, você envolve o nome da pasta com parênteses, como (marketing), e esse nome dentro dos parênteses não aparece na URL. Parece abstrato, então vamos direto ao código:
app/
├── (marketing)/ # Grupo de páginas de marketing
│ ├── layout.tsx # Layout específico do frontend público
│ ├── page.tsx # URL: /
│ ├── about/page.tsx # URL: /about
│ └── products/page.tsx # URL: /products
├── (shop)/ # Grupo de funcionalidades de e-commerce
│ ├── layout.tsx
│ ├── cart/page.tsx # URL: /cart
│ └── checkout/page.tsx # URL: /checkout
└── (dashboard)/ # Grupo de administração
├── layout.tsx
├── dashboard/page.tsx # URL: /dashboard
├── users/page.tsx # URL: /users (não /dashboard/users!)
└── orders/page.tsx # URL: /orders
Repare que (marketing), (shop) e (dashboard) não aparecem na URL. A rota de (marketing)/about/page.tsx continua sendo /about, não /marketing/about.
Você pode pensar: isso não é trabalho extra? Se o nome dentro dos parênteses não afeta a URL, para que serve?
Na prática, o valor central dos route groups está em organizar o código, não em mudar a rota. Eles permitem agrupar rotas por lógica de negócio, divisão de equipe ou módulo funcional. A estrutura de diretórios fica clara sem deixar a URL comprida.
Caso prático: agrupando por equipe
Nossa equipe tinha três pequenos grupos: marketing cuidava do site institucional e das páginas de divulgação, produto cuidava das funcionalidades de e-commerce, e backend cuidava do sistema administrativo. Antes, todo mundo trabalhava no mesmo diretório app, e conflitos de arquivo eram rotina. Depois de usar route groups:
app/
├── (team-marketing)/ # Responsabilidade do time de marketing
│ ├── layout.tsx
│ ├── page.tsx # Página inicial
│ ├── about/page.tsx
│ └── pricing/page.tsx
├── (team-product)/ # Responsabilidade do time de produto
│ ├── layout.tsx
│ ├── products/page.tsx
│ └── product/[id]/page.tsx
└── (team-backend)/ # Responsabilidade do time de backend
├── layout.tsx
├── dashboard/page.tsx
└── admin/page.tsx
Os benefícios ficam bem claros:
-
Menos conflitos de arquivo. O time de marketing trabalha em
(team-marketing), o time de produto trabalha em(team-product), e ninguém pisa no espaço do outro. Na hora de fazer merge no Git, os conflitos diminuem bastante. -
Revisão de código mais clara. Ao olhar um Pull Request, você percebe imediatamente qual área ou equipe será afetada pela mudança.
-
Layouts independentes. Cada route group pode ter seu próprio
layout.tsx. Páginas de marketing usam um layout visual de marketing; páginas do painel usam um layout administrativo. Não é preciso importar manualmente em cada página.
Outro caso: agrupando por tipo de layout
Alguns projetos não se organizam por equipe, mas por tipo de layout:
app/
├── (with-nav)/ # Páginas com navegação superior
│ ├── layout.tsx
│ ├── page.tsx
│ ├── about/page.tsx
│ └── products/page.tsx
├── (fullscreen)/ # Páginas em tela cheia, sem navegação
│ ├── layout.tsx
│ └── video/[id]/page.tsx
└── (auth)/ # Páginas de autenticação com layout simples
├── layout.tsx
├── login/page.tsx
└── register/page.tsx
Páginas de login e cadastro normalmente não precisam de barra de navegação superior nem de rodapé. Usar o route group (auth) permite gerenciá-las separadamente com um layout mais limpo. Páginas de vídeo em tela cheia também podem ficar em um grupo próprio.
Pontos de atenção
Route groups são úteis, mas têm uma armadilha: grupos diferentes não podem conter a mesma rota.
Por exemplo, você não pode ter (marketing)/about/page.tsx e (shop)/about/page.tsx ao mesmo tempo, porque ambos viram /about. O Next.js não saberá qual usar e vai gerar erro.
A solução é planejar bem as rotas e garantir que cada caminho seja único. Se for inevitável, adicione um prefixo real a uma delas, como (shop)/about-us/page.tsx.
Outro ponto: o nome do route group precisa ter sentido. Evite (group1) e (group2), que não dizem nada. Prefira nomes como (marketing), (dashboard) e (auth), que qualquer pessoa da equipe entende de primeira.
Layouts aninhados - herança automática de layout
Como layouts aninhados funcionam
Route groups resolvem a organização dos diretórios, mas ainda existe outro problema: a hierarquia dos layouts. Em um sistema administrativo, por exemplo, é comum ter:
- Primeiro nível: barra superior + sidebar, compartilhadas por todas as páginas administrativas
- Segundo nível: o módulo de usuários tem suas próprias abas, como usuários ativos e usuários bloqueados
- Terceiro nível: conteúdo específico da página
Os layouts aninhados do Next.js foram pensados para esse cenário. Você coloca layout.tsx em pastas de níveis diferentes, e eles se aninham automaticamente:
app/(dashboard)/
├── layout.tsx # Layout de primeiro nível: topo + sidebar
├── users/
│ ├── layout.tsx # Layout de segundo nível: abas de usuários
│ ├── active/page.tsx # /users/active
│ └── blocked/page.tsx # /users/blocked
└── orders/
├── layout.tsx # Layout de segundo nível: abas de pedidos
├── pending/page.tsx
└── completed/page.tsx
Quando o usuário acessa /users/active, a hierarquia de renderização fica assim:
DashboardLayout (primeiro nível)
└─ UsersLayout (segundo nível)
└─ ActiveUsersPage (página)
O código fica assim:
// app/(dashboard)/layout.tsx - layout de primeiro nível
export default function DashboardLayout({
children
}: {
children: React.ReactNode
}) {
return (
<div className="dashboard-container">
<TopBar />
<div className="content-area">
<Sidebar />
<main>{children}</main>
</div>
</div>
)
}
// app/(dashboard)/users/layout.tsx - layout de segundo nível
export default function UsersLayout({
children
}: {
children: React.ReactNode
}) {
return (
<div className="users-section">
<div className="tabs">
<Link href="/users/active">Usuários ativos</Link>
<Link href="/users/blocked">Usuários bloqueados</Link>
</div>
{children}
</div>
)
}
// app/(dashboard)/users/active/page.tsx - página
export default function ActiveUsersPage() {
return <div>Lista de usuários ativos...</div>
}
Percebeu? O componente da página não precisa importar o layout manualmente. O Next.js faz esse encaixe para você.
Vantagem de performance com renderização parcial
O ponto mais poderoso dos layouts aninhados é a renderização parcial (Partial Rendering). Quando você troca de “usuários ativos” para “usuários bloqueados”:
- O layout de primeiro nível, com topo e sidebar, não renderiza novamente
- O layout de segundo nível, com as abas, também não renderiza novamente
- Apenas o conteúdo da página renderiza de novo
Isso significa duas coisas:
-
Performance melhor. Você não renderiza de novo componentes de layout repetidos, então a troca de páginas fica mais rápida.
-
Estado do cliente preservado. Se a sidebar tiver um estado de expandir/recolher, esse estado não se perde ao trocar de página.
Em um projeto anterior, a sidebar do painel tinha uma caixa de busca. Quando o usuário digitava algo e trocava de página, o conteúdo do campo sumia, o que era péssimo. Com layouts aninhados, isso se resolveu naturalmente: a sidebar simplesmente não renderiza de novo, então o estado fica preservado.
Caso prático: navegação em múltiplos níveis
Em projetos reais, é comum ter três ou até quatro níveis de navegação. Por exemplo:
- Administração (layout de primeiro nível: topo + sidebar)
- Gerenciamento de usuários (layout de segundo nível: abas de usuários)
- Usuários ativos (terceiro nível: conteúdo da página)
- Usuários bloqueados (terceiro nível: conteúdo da página)
- Gerenciamento de pedidos (layout de segundo nível: abas de pedidos)
- Pedidos pendentes (terceiro nível: conteúdo da página)
- Pedidos concluídos (terceiro nível: conteúdo da página)
- Gerenciamento de usuários (layout de segundo nível: abas de usuários)
A estrutura de diretórios corresponde exatamente a essa hierarquia, e a lógica do código fica fácil de entender:
app/(dashboard)/
├── layout.tsx # Primeiro nível: topo + sidebar
├── users/
│ ├── layout.tsx # Segundo nível: área de usuários
│ ├── active/page.tsx # Terceiro nível: usuários ativos
│ └── blocked/page.tsx # Terceiro nível: usuários bloqueados
└── orders/
├── layout.tsx # Segundo nível: área de pedidos
├── pending/page.tsx # Terceiro nível: pendentes
└── completed/page.tsx # Terceiro nível: concluídos
Dicas de otimização de performance
Por padrão, layouts aninhados são Server Components. Isso é bom: a lógica de layout renderiza no servidor e não aumenta o JavaScript enviado ao cliente.
Mas se o layout tiver interação, como busca ou menu suspenso, extraia apenas a parte interativa para um Client Component:
// app/(dashboard)/layout.tsx - continua sendo Server Component
import SearchBar from '@/components/SearchBar' // Client Component
export default function DashboardLayout({ children }) {
return (
<div>
<SearchBar /> {/* Client Component */}
<main>{children}</main>
</div>
)
}
// components/SearchBar.tsx - Client Component
'use client'
import { useState } from 'react'
export default function SearchBar() {
const [query, setQuery] = useState('')
// ...lógica de interação
}
Assim você mantém as vantagens do layout no servidor sem abrir mão da interação.
Outra dica é configurar loading.tsx em cada nível de layout para exibir estados de carregamento. A experiência do usuário melhora bastante:
app/(dashboard)/
├── layout.tsx
├── loading.tsx # Carregamento de primeiro nível
└── users/
├── layout.tsx
├── loading.tsx # Carregamento de segundo nível
└── active/
├── page.tsx
└── loading.tsx # Carregamento de terceiro nível
Cada nível pode ter sua própria animação de carregamento, sem interferir nos outros.
Parallel Routes - renderizando várias páginas ao mesmo tempo
Que problema parallel routes resolvem
Uma página de dashboard normalmente mostra vários módulos independentes ao mesmo tempo, por exemplo:
- Canto superior esquerdo: painel de análise de dados
- Canto superior direito: painel da equipe
- Parte inferior: painel de notificações recentes
Cada painel busca seus dados de forma independente, e a velocidade de carregamento também é diferente. A abordagem tradicional é colocar tudo em um único componente de página, mas isso traz um problema: se os dados de um painel forem lentos, a página inteira pode ficar presa no estado de carregamento.
Parallel routes permitem dividir esses módulos em “slots” independentes. Cada slot tem seu próprio estado de carregamento, tratamento de erro e até pode ser renderizado condicionalmente.
Sintaxe básica
Criar parallel routes é simples: nomeie pastas começando com @:
app/dashboard/
├── layout.tsx
├── @analytics/page.tsx # Slot de análise
├── @team/page.tsx # Slot da equipe
├── @notifications/page.tsx # Slot de notificações
└── page.tsx # Página padrão
Repare em @analytics, @team e @notifications: essas pastas com @ são slots.
Depois, em layout.tsx, você pode receber esses slots como props:
// app/dashboard/layout.tsx
export default function DashboardLayout({
children,
analytics,
team,
notifications,
}: {
children: React.ReactNode
analytics: React.ReactNode
team: React.ReactNode
notifications: React.ReactNode
}) {
return (
<div className="dashboard-grid">
<div className="main-content">{children}</div>
<div className="top-panels">
<div className="panel">{analytics}</div>
<div className="panel">{team}</div>
</div>
<div className="bottom-panel">{notifications}</div>
</div>
)
}
Cada slot corresponde a um componente de página independente e pode ter seus próprios estados de loading e error:
app/dashboard/
├── @analytics/
│ ├── page.tsx
│ ├── loading.tsx # Estado de carregamento do painel de análise
│ └── error.tsx # Tratamento de erro do painel de análise
├── @team/
│ ├── page.tsx
│ ├── loading.tsx
│ └── error.tsx
└── @notifications/
├── page.tsx
├── loading.tsx
└── error.tsx
A vantagem é que, se os dados do painel de análise demorarem, apenas esse painel mostra o carregamento; os outros continuam normais. Se um painel falhar, ele também não derruba a página inteira.
Caso prático: renderização condicional
Outro ponto forte de parallel routes é a renderização condicional. Por exemplo, o painel da equipe só aparece para administradores:
// app/dashboard/layout.tsx
import { auth } from '@/lib/auth'
export default async function DashboardLayout({
analytics,
team,
notifications,
}) {
const user = await auth()
const isAdmin = user?.role === 'admin'
return (
<div className="dashboard-grid">
<div>{analytics}</div>
{isAdmin && <div>{team}</div>} {/* Apenas administradores veem */}
<div>{notifications}</div>
</div>
)
}
O papel de default.tsx
Parallel routes têm uma armadilha comum. Quando o usuário navega de /dashboard para /dashboard/settings, talvez um slot não tenha uma página correspondente. O Next.js não sabe o que renderizar e pode gerar erro.
A solução é criar default.tsx como conteúdo de fallback:
// app/dashboard/@team/default.tsx
export default function Default() {
return null // Ou retorne um componente placeholder
}
Com default.tsx, quando o slot não tiver página correspondente, esse fallback será renderizado e o erro será evitado.
Quando usar parallel routes
Para ser honesto, parallel routes não são tão amplamente aplicáveis quanto route groups e layouts aninhados. Elas servem bem para:
- Dashboards com vários módulos: vários painéis de dados independentes que precisam carregar separadamente
- Testes A/B: exibir conteúdos diferentes conforme o grupo do usuário
- Controle de permissões: renderizar seletivamente certos slots conforme a permissão do usuário
Mas se o conteúdo é apenas uma sequência simples de blocos e não precisa de estados independentes de carregamento, escreva direto no componente da página. Não complique com parallel routes.
Intercepting Routes - implementando rotas de modal
A experiência no estilo Instagram
Falando francamente, intercepting routes são o recurso mais difícil de entender entre esses quatro. Na primeira vez que li a documentação, não entendi que problema elas resolviam, até ver o exemplo do Instagram.
Você está navegando pelo feed no Instagram, clica em uma foto, e ela abre ampliada em um modal enquanto a URL vira /photo/abc123. Nesse momento:
- Se você atualizar a página, o modal desaparece e a página completa da foto aparece
- Se você compartilhar a URL com alguém, a outra pessoa abre a página completa da foto, não o modal
- Se você clicar em fechar, o modal some e você volta ao feed
O benefício dessa experiência é evidente: a URL é compartilhável, atualizar a página não perde o contexto e a navegação no cliente continua fluida. A abordagem tradicional tem dificuldade para reproduzir isso.
Intercepting routes existem para resolver esse problema.
Sintaxe básica
Intercepting routes usam uma forma especial de nomear pastas:
(.)corresponde a uma rota no mesmo nível(..)corresponde a uma rota um nível acima(..)(..)corresponde a uma rota dois níveis acima(...)corresponde a uma rota a partir da raiz
Parece abstrato, então veja o código:
app/
├── products/
│ ├── page.tsx # Página de lista de produtos
│ └── (..)product/[id]/page.tsx # Intercepta /product/123 e mostra como modal
└── product/
└── [id]/page.tsx # Página completa de detalhe do produto
Quando o usuário está em /products e clica em um link de produto (<Link href="/product/123">):
- Navegação no cliente: a interceptação é acionada e
(..)product/[id]/page.tsxé renderizado como versão modal - Acesso direto a
/product/123ou atualização da página: a interceptação não é acionada, eproduct/[id]/page.tsxrenderiza a página completa
Caso prático: modal de detalhe do produto
Em um e-commerce que fiz antes, havia este requisito: ao clicar em um produto na lista, abrir um modal com os detalhes.
Estrutura de diretórios:
app/
├── (shop)/
│ └── products/
│ ├── page.tsx # Lista de produtos
│ └── (..)product/[id]/page.tsx # Versão modal
└── product/
└── [id]/page.tsx # Versão de página completa
Código da versão modal:
// app/(shop)/products/(..)product/[id]/page.tsx
'use client'
import { useRouter } from 'next/navigation'
import Modal from '@/components/Modal'
import ProductDetail from '@/components/ProductDetail'
export default function ProductModal({
params
}: {
params: { id: string }
}) {
const router = useRouter()
return (
<Modal onClose={() => router.back()}>
<ProductDetail id={params.id} />
</Modal>
)
}
Versão de página completa:
// app/product/[id]/page.tsx
import ProductDetail from '@/components/ProductDetail'
export default function ProductPage({
params
}: {
params: { id: string }
}) {
return (
<div className="product-page">
<ProductDetail id={params.id} />
</div>
)
}
Note que o componente de detalhe (ProductDetail) é reutilizado. Só o contêiner externo muda: um é modal, o outro é página completa.
Usando junto com parallel routes
Usar intercepting routes isoladamente às vezes traz problemas de gerenciamento de estado. Uma abordagem melhor é combiná-las com parallel routes:
app/(shop)/products/
├── layout.tsx
├── page.tsx
├── @modal/
│ ├── (..)product/[id]/page.tsx # Slot do modal
│ └── default.tsx # Vazio por padrão
Componente de layout:
// app/(shop)/products/layout.tsx
export default function ProductsLayout({
children,
modal,
}: {
children: React.ReactNode
modal: React.ReactNode
}) {
return (
<>
{children}
{modal}
</>
)
}
// app/(shop)/products/@modal/default.tsx
export default function Default() {
return null // Retorna null quando não há modal
}
Assim, o modal e o conteúdo principal ficam completamente separados, o gerenciamento de estado fica mais claro e a estrutura também fica mais fácil de entender.
Pontos de atenção
Intercepting routes têm alguns detalhes importantes:
-
Só interceptam durante navegação no cliente. Se o usuário digitar a URL diretamente na barra do navegador ou atualizar a página, a interceptação não acontece.
-
É preciso manter duas versões. A versão modal e a versão de página completa precisam existir. Embora você possa reutilizar componentes, ainda há alguma duplicação.
-
Regra de correspondência de caminho.
(..)é baseado no caminho da rota, não no caminho do sistema de arquivos. Se você usa route groups, lembre-se de que eles não afetam a URL; portanto, a correspondência pode ser diferente do que você imagina.
Por exemplo:
app/
├── (shop)/products/
│ └── (..)product/[id]/page.tsx # Intercepta /product/[id]
Aqui, (..) sobe um nível a partir de /products até a raiz, então corresponde a /product/[id], não a /(shop)/product/[id].
Quando usar intercepting routes
Intercepting routes são adequadas para:
- Galerias de imagens: clicar em uma imagem da lista e abrir um modal com a imagem ampliada
- Detalhes de produto: clicar em um produto da lista e abrir um modal com os detalhes
- Janela de login: botão de login na navegação abre um modal, mas a rota
/logintambém pode ser acessada diretamente
Elas não são adequadas para:
- Pop-ups simples que não precisam mudar a URL
- Cenários que não precisam de deep link
No geral, intercepting routes são um recurso poderoso, mas também o mais complexo. Se o seu projeto não tem uma necessidade parecida com a do Instagram, tudo bem não usá-las por enquanto.
Prática integrada - estrutura completa de diretórios para e-commerce
Até aqui, falamos dos quatro recursos. Agora vamos combiná-los e ver como organizar um projeto real de e-commerce.
Requisitos do projeto
Um e-commerce típico costuma ter estes módulos:
Frontend público, voltado ao usuário:
- Página inicial e “sobre nós” como páginas de marketing
- Lista de produtos e detalhe do produto, com suporte a modal
- Carrinho e checkout
Painel administrativo, voltado a administradores:
- Dashboard com vários módulos: análise de dados, equipe e notificações
- Gerenciamento de usuários: ativos e bloqueados
- Gerenciamento de pedidos: pendentes e concluídos
Autenticação:
- Login e cadastro, com layout independente e sem barra de navegação
Estrutura final de diretórios
app/
├── layout.tsx # Layout raiz
│
├── (marketing)/ # Route group do frontend público
│ ├── layout.tsx # Layout do frontend público (topo + rodapé)
│ ├── page.tsx # / (página inicial)
│ ├── about/page.tsx # /about
│ └── pricing/page.tsx # /pricing
│
├── (shop)/ # Grupo de funcionalidades de e-commerce
│ ├── layout.tsx # Layout do e-commerce
│ ├── products/
│ │ ├── layout.tsx # Layout da lista de produtos (com slot de modal)
│ │ ├── page.tsx # /products
│ │ └── @modal/
│ │ ├── (..)product/[id]/page.tsx # Modal de detalhe do produto
│ │ └── default.tsx
│ ├── cart/page.tsx # /cart
│ └── checkout/page.tsx # /checkout
│
├── product/
│ └── [id]/page.tsx # /product/123 (página completa)
│
├── (dashboard)/ # Route group do painel administrativo
│ ├── layout.tsx # Layout do painel (sidebar + topo)
│ ├── dashboard/
│ │ ├── layout.tsx # Layout do dashboard (parallel routes)
│ │ ├── page.tsx # /dashboard (conteúdo padrão)
│ │ ├── @analytics/
│ │ │ ├── page.tsx # Módulo de análise de dados
│ │ │ ├── loading.tsx
│ │ │ └── default.tsx
│ │ ├── @team/
│ │ │ ├── page.tsx # Módulo da equipe
│ │ │ ├── loading.tsx
│ │ │ └── default.tsx
│ │ └── @notifications/
│ │ ├── page.tsx # Módulo de notificações
│ │ ├── loading.tsx
│ │ └── default.tsx
│ ├── users/
│ │ ├── layout.tsx # Layout secundário de usuários
│ │ ├── active/page.tsx # /users/active
│ │ └── blocked/page.tsx # /users/blocked
│ └── orders/
│ ├── layout.tsx # Layout secundário de pedidos
│ ├── pending/page.tsx # /orders/pending
│ └── completed/page.tsx # /orders/completed
│
└── (auth)/ # Route group de autenticação
├── layout.tsx # Layout simples, sem navegação
├── login/page.tsx # /login
└── register/page.tsx # /register
Comparação com a estrutura tradicional
Vamos comparar a estrutura plana tradicional com a nova estrutura:
| Dimensão | Estrutura plana tradicional | Route groups + layouts aninhados |
|---|---|---|
| Encontrar arquivos | Precisa procurar em 100+ arquivos, diferenciando por prefixos | Agrupamento por módulo de negócio, claro de primeira |
| Gerenciamento de layout | Cada página importa o layout manualmente | Herança automática; alterar um ponto afeta tudo |
| Colaboração da equipe | Todo mundo trabalha no mesmo diretório, com mais conflitos | Equipes ou módulos diferentes ficam em pastas diferentes |
| Clareza da URL | Precisa de prefixos para evitar conflito (dashboard-users-active) | URLs simples (/users/active) e estrutura clara |
| Experiência de modal | Estado no cliente; atualização da página perde o estado | Rotas dirigem a experiência; atualização mostra a página completa |
| Performance | Troca de página renderiza layouts repetidos | Renderização parcial; só a parte que muda renderiza |
Benefícios concretos
Depois de adotar a nova estrutura, nossa equipe percebeu ganhos reais:
-
Velocidade para encontrar arquivos 50% maior. Antes, procurar uma página exigia abrir várias pastas. Agora basta ir ao route group correspondente.
-
Mais eficiência ao alterar layouts. Antes, mudar a sidebar do painel exigia conferir 20 arquivos; agora basta alterar
(dashboard)/layout.tsx, e todas as páginas administrativas recebem a mudança automaticamente. -
Conflitos de código 60% menores. Marketing trabalha em
(marketing), produto em(shop)e backend em(dashboard). Cada um mexe na sua área. -
Onboarding mais rápido. Um estagiário recém-chegado olhou a estrutura de diretórios e entendeu a arquitetura do projeto em cinco minutos, sem eu precisar explicar muito.
Algumas recomendações práticas
-
Não refatore tudo de uma vez. Escolha um módulo, como o painel administrativo, faça um piloto e só depois leve a abordagem para o projeto inteiro.
-
Dê nomes significativos aos route groups. Usamos
(marketing),(shop),(dashboard)e(auth), e qualquer pessoa da equipe entende de primeira. -
Documentação é importante. Adicione ao README do projeto uma seção “Estrutura de diretórios”, explicando a responsabilidade de cada route group para facilitar o entendimento de quem entra depois.
-
Combine com TypeScript. Route groups e layouts aninhados ficam ainda mais claros quando combinados com mapeamento de caminhos do TypeScript, como
@/*.
// tsconfig.json
{
"compilerOptions": {
"paths": {
"@/*": ["./src/*"],
"@/components/*": ["./src/components/*"],
"@/app/*": ["./src/app/*"]
}
}
}
Boas práticas e pontos de atenção
Padrões de nomeação para route groups
O nome de um route group afeta diretamente a manutenibilidade do código. Nossa equipe resumiu algumas regras:
Nomes recomendados:
(marketing)- páginas relacionadas a marketing(dashboard)ou(admin)- administração interna(auth)- autenticação(team-xxx)- quando o agrupamento é por equipe(feature-xxx)- quando o agrupamento é por funcionalidade
Evite usar:
(group1),(group2)- nomes sem significado(temp),(test)- nomes temporários- Nomes longos demais, como
(marketing-and-sales-pages)- pouco concisos
Lembre-se: o nome do route group existe apenas para quem desenvolve. Ele não aparece na URL, então escolha um nome que a equipe entenda imediatamente.
Evitando conflitos de rota
Route groups diferentes não podem ter o mesmo caminho de rota. Esse é o tropeço mais comum:
❌ Exemplo incorreto:
app/
├── (marketing)/about/page.tsx # URL: /about
└── (shop)/about/page.tsx # URL: /about - conflito!
Soluções:
- Planeje as rotas: antes de começar, desenhe um mapa de rotas e garanta que todos os caminhos sejam únicos
- Adicione prefixos: se você realmente precisar de dois about, diferencie-os, como
/about-use/about-product - Ajuste a hierarquia: mova as rotas conflitantes para níveis diferentes
Quando usar parallel routes
Parallel routes não são obrigatórias. Use apenas nestes cenários:
Cenários adequados:
- Vários painéis independentes em um dashboard
- Conteúdos paralelos que precisam de estados de carregamento independentes
- Renderização de módulos diferentes conforme permissões do usuário
- Testes A/B com variações de conteúdo
Cenários inadequados:
- Conteúdo simples empilhado verticalmente, que pode ser escrito direto na página
- Conteúdo que não precisa de estado de carregamento independente
- Áreas estáticas de layout
Se você não tem certeza se precisa usar parallel routes, provavelmente não precisa. É um recurso avançado; na maioria dos projetos, route groups e layouts aninhados já dão conta.
Limitações de intercepting routes
Intercepting routes são poderosas, mas há alguns pontos importantes:
-
Só interceptam durante navegação no cliente. Se o usuário digitar a URL diretamente ou atualizar a página, a interceptação não acontece.
-
É preciso manter duas versões. A versão modal e a versão de página completa precisam ser implementadas. Mesmo reutilizando componentes, ainda existe custo de manutenção.
-
A correspondência de caminho é baseada na rota, não no sistema de arquivos. Route groups não afetam a URL, então
(..)corresponde ao caminho da URL, não ao caminho da pasta. Isso é fácil de confundir.
Se o seu requisito não precisa alterar a URL ou não precisa de deep link, um modal comum no cliente já resolve. Não é necessário usar intercepting routes.
Dicas de otimização de performance
-
Mantenha componentes de layout como Server Components. Por padrão, layouts são Server Components. Preserve isso sempre que possível e extraia apenas as partes interativas para Client Components.
-
Use loading.tsx. Adicione
loading.tsxem cada nível de rota para oferecer estados de carregamento amigáveis. A experiência do usuário melhora bastante. -
Use Suspense com critério. Combinado com
loading.tsx, Suspense permite renderização em streaming mais refinada. -
Evite aninhamento excessivo. Não deixe a hierarquia de layouts passar de 4 níveis. Profundidade demais aumenta a complexidade e prejudica a manutenção.
Estratégia de migração
Se você está migrando do Pages Router para o App Router, recomendo seguir este caminho:
-
Migração incremental: os diretórios
appepagespodem coexistir. Migre um módulo por vez, sem mudar tudo de uma vez. -
Migre primeiro os layouts: refatore a lógica de layout com route groups e layouts aninhados. Essa costuma ser a parte de maior retorno.
-
Depois migre a busca de dados: troque
getServerSidePropsporfetchegetStaticPropspor Server Components. -
Por fim, migre as rotas: substitua
getStaticPathsporgenerateStaticParams. -
Faça rollout gradual: use feature flags para controlar a troca entre rotas novas e antigas. Se algo der errado, você consegue voltar rápido.
Recomendações para colaboração da equipe
-
Defina uma convenção de estrutura de diretórios. Escreva no README ou na Wiki as responsabilidades de cada route group, as regras de nomeação e o fluxo para adicionar novas páginas.
-
Foque nisso na revisão de código. Ao revisar PRs, verifique se as regras de rota foram seguidas, se há conflitos de rota e se os layouts estão aninhados corretamente.
-
Use regras de ESLint. Você pode configurar ESLint para verificar nomes de route groups, proibir certos caminhos e assim por diante.
-
Refatore periodicamente. Ao fim de cada iteração, reserve tempo para organizar a estrutura de diretórios, remover páginas abandonadas e renomear route groups mal escolhidos.
Dicas de depuração
Depurar route groups e layouts aninhados às vezes é um pouco trabalhoso. Aqui vão algumas dicas:
-
Veja o React DevTools. Na aba React das ferramentas de desenvolvedor do navegador, você pode ver a árvore completa de componentes e confirmar se os layouts estão aninhados corretamente.
-
Use console.log. Adicione
console.lognos componentes de layout para ver se eles renderizam novamente e se cada navegação dispara uma renderização. -
Confira a Network tab. Veja quais requisições partem do servidor e quais partem do cliente para confirmar se a lógica de busca de dados está correta.
-
Leia a saída do Next.js. Em modo de desenvolvimento, o Next.js mostra várias informações úteis, como conflitos de rota e layouts ausentes. Preste atenção aos avisos e erros do terminal.
Conclusão
Voltando à cena do início do artigo: uma da manhã, você encarando 120 pastas amontoadas e gastando cinco minutos para encontrar uma página.
Se o seu projeto Next.js passa pelo mesmo problema, os quatro recursos deste artigo podem ajudar:
Route groups permitem agrupar rotas por lógica de negócio ou divisão de equipe. A estrutura fica clara sem deixar a URL longa.
Layouts aninhados cuidam automaticamente da herança de layout. Você não precisa importar o layout em cada página, e uma mudança pode ser feita em um único arquivo.
Parallel routes permitem renderizar vários módulos independentes ao mesmo tempo, cada um com seu próprio carregamento e tratamento de erro. São úteis em dashboards com vários painéis.
Intercepting routes criam uma experiência de modal parecida com a do Instagram: URL compartilhável, atualização sem perda de contexto e navegação no cliente fluida.
Minha sugestão é começar pequeno. Escolha um módulo, como o painel administrativo, faça uma experiência com route groups e layouts aninhados e veja o resultado. Depois de validar, expanda para o restante do projeto.
Não refatore todo o código de uma vez. O risco é alto demais. Faça uma migração incremental, passo a passo; se algo der errado, fica muito mais fácil reverter.
Por fim, deixei uma estrutura completa de diretórios para e-commerce que você pode usar como referência ou adaptar diretamente. Se tiver dúvidas, fique à vontade para comentar e discutir.
Que o diretório do seu projeto Next.js finalmente deixe a bagunça para trás e fique muito mais fácil de manter!
Fluxo completo para refatorar a estrutura de diretórios de projetos grandes em Next.js
Use route groups, layouts aninhados, parallel routes e intercepting routes para refatorar uma estrutura de diretórios confusa
⏱️ Estimated time: 8 hr
- 1
Step 1: Analisar a estrutura de diretórios atual
Avalie os problemas atuais:
• Conte a quantidade de pastas (se passar de 50, considere refatorar)
• Identifique pontos de conflito de rota
• Encontre códigos de layout repetidos
• Registre os pontos de dor na colaboração da equipe
Identifique áreas funcionais:
• Páginas de marketing (home, sobre, preços)
• Páginas de loja (produtos, carrinho, pedidos)
• Administração interna (usuários, pedidos, configurações) - 2
Step 2: Criar route groups para separar áreas funcionais
Use parênteses para criar route groups:
• (marketing): páginas relacionadas a marketing
• (shop): páginas relacionadas à loja
• (dashboard): páginas de administração
Pontos de atenção:
• O nome do route group não afeta a URL
• A mesma URL não pode aparecer em vários grupos
• Cada route group pode ter seu próprio layout.js - 3
Step 3: Projetar a hierarquia de layouts aninhados
Projete de acordo com a hierarquia da UI:
• Primeira camada: app/layout.js (comum ao site todo)
• Segunda camada: layout.js da área funcional (por exemplo, shop/layout.js)
• Terceira camada: layout.js da página de detalhe (por exemplo, shop/products/[id]/layout.js)
Pontos de implementação:
• Cada camada adiciona apenas os elementos de UI específicos dela
• Layouts filhos herdam automaticamente os layouts pais
• Ao trocar de página, o layout não renderiza de novo - 4
Step 4: Implementar parallel routes, se necessário
Use @ para criar slots:
• @modal: slot de modal
• @sales, @orders: módulos independentes de dashboard
Receba no layout.js:
• export default function Layout({ children, modal })
• Renderize no JSX: {modal}
Cada slot pode ter seu próprio loading.js e error.js - 5
Step 5: Implementar intercepting routes, se precisar de modal
Crie a rota interceptada:
• Em @modal, crie (.)photos/[id]/page.js
• Crie photos/[id]/page.js (página completa)
Sintaxe:
• (.): intercepta rota no mesmo nível
• (..): intercepta rota um nível acima
• (...): intercepta a partir da raiz
É obrigatório criar default.js retornando null - 6
Step 6: Testar e validar
Pontos de validação:
• Todas as rotas funcionam corretamente?
• Os layouts estão aninhados corretamente?
• A troca de páginas está fluida?
• O modal funciona normalmente?
Ferramentas de depuração:
• React DevTools para ver a árvore de componentes
• console.log para conferir contagem de renderizações
• Network tab para conferir requisições
• Saída do terminal do Next.js para verificar avisos
FAQ
Quando devo usar route groups?
Route groups afetam a URL?
Layouts aninhados afetam a performance?
Qual é a diferença entre parallel routes e componentes comuns?
Como escolher a sintaxe de intercepting routes?
Como evitar conflitos de rota?
Quanto tempo leva para refatorar um projeto grande?
25 min de leitura · Publicado em: 18 dez 2025 · Atualizado em: 14 jul 2026
Guia completo Next.js
Se você chegou pela busca, o caminho mais rápido é ir para o post anterior ou próximo desta série.
Anterior
Rotas avançadas no Next.js: guia completo de grupos, layouts aninhados, rotas paralelas e interceptação
Aprenda em profundidade quatro recursos avançados de roteamento do Next.js: grupos de rotas para organizar diretórios, layouts aninhados para reutilização flexível, rotas paralelas para exibir várias páginas e rotas interceptadas para criar modais elegantes. Inclui exemplos completos e cuidados práticos para resolver a desorganização das rotas e os conflitos de colaboração em projetos maiores.
Parte 4 de 51
Próximo
Rotas dinâmicas e parâmetros no Next.js: do básico à segurança de tipos
Aprenda a trabalhar com rotas dinâmicas no Next.js 14+, incluindo parâmetros, rotas catch-all, parâmetros opcionais, generateStaticParams e segurança de tipos com TypeScript.
Parte 6 de 51



Comentários
Entre com GitHub para comentar