Cambiar tema

Next.js: rutas avanzadas en la práctica — grupos, layouts anidados, rutas paralelas e interceptadas

Easton editorial illustration: server-client bridge

La semana pasada asumí un proyecto e-commerce en Next.js con dos años en producción. Al abrir app, más de 60 carpetas apretadas en pantalla: about, products, admin-users, marketing-campaign, shop-cart… todo en un solo nivel. ¿Buscar una página de usuarios? Hay que rebuscar entre páginas de marketing. Peor aún: tres personas tocando rutas a la vez, al menos dos conflictos de Git al día; en code review, media hora solo para entender qué archivo hace qué.

Sentado frente al monitor, recordé que Next.js ya tenía grupos de rutas, rutas paralelas y más. Revisé la documentación: existían desde Next.js 13, pero el proyecto no los usaba. No faltaban herramientas; faltaba saber cuándo aplicar cada una.

Esa noche invertí tres horas en estudiarlas y dos días en reestructurar. El directorio quedó limpio, el equipo dejó de gritar «otro conflicto» en el chat. Sobre todo, entendí para qué sirven estas cuatro piezas que al principio parecen enrevesadas: grupos de rutas para ordenar carpetas, layouts anidados para reutilizar estructura, rutas paralelas para mostrar varias vistas a la vez e rutas interceptadas para modales elegantes.

Si tu proyecto Next.js crece, los archivos se amontonan y el equipo choca en Git, al terminar este artículo sabrás cuándo usar cada técnica, cómo implementarla y qué trampas evitar.

Grupos de rutas (Route Groups): directorios ordenados

¿Qué son los grupos de rutas?

En resumen: carpetas entre paréntesis, como (marketing) o (shop). Next.js no añade ese nombre a la URL.

¿Parece poco útil? Espera a un escenario real.

Imagina un e-commerce con páginas de marketing (inicio, about), tienda (listado, carrito) y panel (pedidos, usuarios). Lo clásico: todo en la raíz de app o forzar prefijos /marketing, /shop, /admin en la URL. ¿Quién quiere yoursite.com/marketing/about?

Los grupos de rutas resuelven el dilema: organizas archivos por función sin cambiar lo que ve el usuario.

Tres usos clave (todos prácticos)

Uso 1: organizar por equipo o función

El beneficio más directo. De 60 carpetas planas a tres grupos:

app/
├── (marketing)/    # Equipo de marketing
│   ├── page.js     # Inicio → yoursite.com/
│   ├── about/      # About → yoursite.com/about
│   └── pricing/    # Precios → yoursite.com/pricing
├── (shop)/         # Equipo frontend
│   ├── products/   # Productos → yoursite.com/products
│   └── cart/       # Carrito → yoursite.com/cart
└── (dashboard)/    # Equipo backend
    ├── orders/     # Pedidos → yoursite.com/orders
    └── users/      # Usuarios → yoursite.com/users

La URL sigue limpia; la estructura de archivos es obvia. Un becario entiende de un vistazo qué carpeta toca qué.

Uso 2: layouts raíz distintos por zona

Aquí brilla el grupo de rutas. ¿La barra de marketing y la del panel pueden ser iguales? No. Pero /about y /orders empiezan en la raíz. ¿Cómo darles layouts distintos?

Respuesta: cada grupo puede tener su layout.js.

app/
├── (marketing)/
│   ├── layout.js        # Marketing: nav superior + hero
│   └── ...
├── (shop)/
│   ├── layout.js        # Tienda: icono carrito + filtros
│   └── ...
└── (dashboard)/
    ├── layout.js        # Panel: sidebar + control de permisos
    └── ...

Tres layouts, sin interferencias. Hero en marketing, sidebar en panel, contador de carrito en tienda — un solo proyecto, sin subdominios ni varias instancias de Next.js.

Uso 3: compartir layout de forma selectiva

A veces «estas páginas comparten layout, otras no». Los artículos del blog llevan índice lateral; la portada del blog, no. Con un grupo:

app/
├── blog/
│   ├── page.js         # Portada del blog, sin sidebar
│   └── (articles)/     # Artículos con sidebar compartido
│       ├── layout.js   # Layout con navegación lateral
│       ├── [slug]/     # Detalle → /blog/xxx
│       └── ...

(articles) no cambia la URL: sigue siendo /blog/my-first-post, pero solo esas páginas usan el layout con navegación.

Caso real: de caos a claridad

El proyecto e-commerce con 60 carpetas. Antes y después:

Antes (extracto):

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

¿Buscar un archivo? Ctrl+F. ¿A qué módulo pertenece? Adivina por el nombre.

Después:

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

Tres capas, claras. Marketing en (marketing); funciones de panel en (dashboard). En code review cada equipo mira su grupo; los conflictos bajaron un 70 %.

Tres trampas a evitar

Trampa 1: conflicto de URL

Los grupos no cambian la URL. ¿Dos grupos con la misma ruta?

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

Next.js lanza Error: Conflicting route. Solución: renombrar o añadir un segmento real (sin paréntesis):

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

Trampa 2: varios layouts raíz provocan recarga completa

Al ir de (shop) a (marketing) la página «parpadea». No es bug: es diseño.

Layouts raíz distintos son independientes; Next.js desmonta uno y monta otro (full page load). Si quieres transición suave, sube lo compartido a app/layout.js y deja en cada grupo solo las diferencias.

Trampa 3: ubicación de la página de inicio

Con varios grupos y cada uno con layout.js, page.js de inicio debe ir dentro de un grupo, no en app/page.js. Si no, Next.js no sabe qué layout raíz usar.

Lo habitual: inicio en (marketing)/page.js.


En una frase: organizan archivos, no tocan la URL y permiten layouts distintos por zona. Proyectos pequeños no lo necesitan; con más de 20 carpetas en app, merece la pena probar.

Layouts anidados (Nested Layouts): reutilizar estructura de página

¿Qué problema resuelven?

Inicio: solo nav superior. Listado del blog: nav + sidebar de categorías. Artículo: nav + sidebar + índice lateral.

Con componentes sueltos, montas todo en cada página. ¿Cambias el nav? Tres sitios.

Los layouts anidados de Next.js hacen que cada carpeta pueda envolver a las hijas con su layout, como muñecas rusas. Cada nivel añade UI sin repetir la exterior.

¿Cómo funcionan?

Cada carpeta puede tener layout.js; las hijas heredan y añaden otra capa.

Ejemplo: plataforma de cursos online:

app/
├── layout.js              # Raíz: nav + Footer
└── courses/
    ├── layout.js          # Cursos: raíz + categorías laterales
    ├── page.js            # Listado de cursos
    └── [id]/
        ├── layout.js      # Detalle: + barra de progreso
        └── page.js        # Curso concreto

En /courses/123:

  1. Exterior: app/layout.js (nav + Footer)
  2. Medio: courses/layout.js (sidebar de categorías)
  3. Interior: courses/[id]/layout.js (barra de progreso)
  4. Contenido: courses/[id]/page.js

Cambias el nav solo en app/layout.js; afecta a todo.

Caso práctico: blog en tres capas

Requisitos:

  • Todas las páginas: nav (inicio, about, contacto) + pie
  • Blog: nav + filtro de categorías lateral
  • Artículo: nav + categorías + índice de anclas

Con layouts anidados queda natural:

app/
├── layout.js                    # Capa 1: sitio
│   └── <Header /><Footer />
└── blog/
    ├── layout.js                # Capa 2: blog
    │   └── <Sidebar />
    ├── page.js                  # Listado (hereda 1 y 2)
    └── [slug]/
        ├── layout.js            # Capa 3: artículo
        │   └── <TableOfContents />
        └── page.js              # Detalle (hereda las tres)

Código simplificado:

app/layout.js (capa 1)

export default function RootLayout({ children }) {
  return (
    <html>
      <body>
        <Header />
        {children}   {/* Renderiza layout o página hijo */}
        <Footer />
      </body>
    </html>
  )
}

app/blog/layout.js (capa 2)

export default function BlogLayout({ children }) {
  return (
    <div className="blog-container">
      <Sidebar />
      <main>{children}</main>
    </div>
  )
}

app/blog/[slug]/layout.js (capa 3)

export default function ArticleLayout({ children }) {
  return (
    <div className="article-container">
      {children}
      <TableOfContents />
    </div>
  )
}

Cada layout solo añade su UI; Next.js anida el resto automáticamente.

Combinados con grupos de rutas

Los grupos aislan en horizontal (marketing, tienda, panel); los layouts apilan en vertical.

En la zona tienda del e-commerce:

app/
└── (shop)/
    ├── layout.js             # Tienda: carrito + nav de categorías
    ├── products/
    │   ├── layout.js         # Listado: + filtros laterales
    │   ├── page.js
    │   └── [id]/
    │       ├── layout.js     # Detalle: + migas de pan
    │       └── page.js
    └── cart/
        └── page.js           # Solo layout raíz de tienda
  • /products: raíz tienda + filtros
  • /products/123: raíz + filtros + migas
  • /cart: solo raíz de tienda

Qué UI lleva cada página lo decide la jerarquía de carpetas, no if (esDetalle).

Dos detalles importantes

Detalle 1: el layout no se vuelve a renderizar

De /blog a /blog/my-post, app/layout.js y app/blog/layout.js no se re-renderizan: scroll y estado del sidebar se conservan; solo cambia el page.js interior.

Detalle 2: el layout no recibe params de rutas hijas

En app/products/[id]/page.js, [id] solo está en page.js vía params, no en layout.js.

Si el layout necesita el id (p. ej. título del producto), usa Context o sube los datos.


La idea: la estructura de carpetas refleja la jerarquía visual. Sin copiar componentes; un layout.js actualiza toda una zona.

Rutas paralelas (Parallel Routes): varias páginas a la vez

¿Cuándo las necesitas?

Panel con tres módulos simultáneos:

  • Arriba izquierda: gráfico de ventas
  • Arriba derecha: pedidos recientes
  • Abajo: alertas de stock

Datos independientes, velocidades distintas. Lo clásico: tres fetch en dashboard/page.js; un fallo tumba todo; loading manual por módulo.

Rutas paralelas renderizan varios fragmentos de página (slots) en la misma vista, cada uno con loading, error y navegación propios.

Sintaxis: @ define el slot

Carpetas @nombre son slots:

app/
└── dashboard/
    ├── layout.js          # Recibe slots como props
    ├── @sales/            # Slot 1: ventas
    │   └── page.js
    ├── @orders/           # Slot 2: pedidos
    │   └── page.js
    ├── @inventory/        # Slot 3: inventario
    │   └── page.js
    └── page.js            # Contenido principal (opcional)

En layout.js, Next.js pasa los slots como props:

export default function DashboardLayout({
  children,    // page.js
  sales,       // @sales/page.js
  orders,      // @orders/page.js
  inventory    // @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>
  )
}

Tres «subpáginas» que colocas donde quieras, sin un page.js monolítico.

Caso práctico: panel de administración

@sales/page.js

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

export default async function SalesWidget() {
  const data = await getSalesData()
  return (
    <div>
      <h3>Ventas del mes</h3>
      <Chart data={data} />
    </div>
  )
}

@orders/page.js

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

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

@inventory/page.js

export default function InventoryWidget() {
  return (
    <div>
      <h3>Alertas de stock</h3>
      <p>5 productos con stock bajo</p>
    </div>
  )
}

Los tres slots cargan en paralelo. Pedidos rápido → se pinta primero; ventas lento → espera. Un slot falla → los demás siguen.

loading.js y error.js por slot

app/
└── dashboard/
    ├── @sales/
    │   ├── page.js
    │   ├── loading.js
    │   └── error.js
    ├── @orders/
    │   ├── page.js
    │   └── loading.js
    └── @inventory/
        └── page.js

@sales/loading.js:

export default function SalesLoading() {
  return <div>Cargando datos de ventas...</div>
}

@sales/error.js:

'use client'

export default function SalesError({ error, reset }) {
  return (
    <div>
      <p>Error al cargar ventas</p>
      <button onClick={reset}>Reintentar</button>
    </div>
  )
}

Ventas en «cargando», pedidos ya visibles, inventario al instante. Si la API de ventas falla, solo ese widget muestra error.

default.js

En /dashboard navegas a /dashboard/settings. Los slots @sales, @orders no coinciden con la nueva ruta.

Por defecto Next.js mantiene el contenido anterior. Para vaciar el slot al cambiar de ruta, usa default.js:

app/
└── dashboard/
    ├── @sales/
    │   ├── page.js
    │   └── default.js
    └── ...

@sales/default.js:

export default function SalesDefault() {
  return null
}

En /dashboard/settings, @sales renderiza null.

El uso más común junto a rutas interceptadas para modales (siguiente sección). También brillan en dashboards con módulos independientes.

Rutas interceptadas (Intercepting Routes): modales sin dolor

La experiencia de Instagram

En Instagram o redes similares: tocas una foto en el feed, abre un modal y la URL pasa a /photo/abc123.

  • Atrás en el navegador: cierra el modal, vuelves al feed
  • Recargas: página completa de la foto, sin modal
  • Compartes la URL: el otro ve la página completa

Lo clásico implica mucho estado, parsing de URL y history manual.

Rutas interceptadas interceptan la navegación en cliente y muestran el destino en modal; con acceso directo o recarga, renderizan la página completa.

Sintaxis: (.) y variantes

  • (.) — mismo nivel
  • (..) — nivel superior
  • (..)(..) — dos niveles arriba
  • (...) — desde la raíz

Lista en /photos, detalle en /photos/123. Quieres modal al clic y página completa al recargar:

app/
├── @modal/
│   ├── (.)photos/
│   │   └── [id]/
│   │       └── page.js   # Contenido del modal
│   └── default.js
├── layout.js
├── page.js               # Feed
└── photos/
    └── [id]/
        └── page.js       # Página completa

@modal/(.)photos/ intercepta photos al mismo nivel que @modal bajo app/.

Caso completo: modal de fotos estilo Instagram

  • Grid en inicio
  • Clic → modal, URL /photos/123
  • Recarga o URL directa → página completa
  • Atrás cierra el modal

Estructura

app/
├── @modal/
│   ├── (.)photos/
│   │   └── [id]/
│   │       └── page.js
│   └── default.js
├── layout.js
├── page.js
└── photos/
    └── [id]/
        └── page.js

app/layout.js:

export default function RootLayout({ children, modal }) {
  return (
    <html>
      <body>
        {children}
        {modal}
      </body>
    </html>
  )
}

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>
  )
}

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()}>Cerrar</button>
        <Image src={`/images/photo${params.id}.jpg`} fill />
      </div>
    </div>
  )
}

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

import Image from 'next/image'

export default function PhotoPage({ params }) {
  return (
    <div className="photo-page">
      <nav>Volver al inicio</nav>
      <h1>Detalle de la imagen</h1>
      <Image src={`/images/photo${params.id}.jpg`} width={800} height={600} />
      <p>Descripción...</p>
    </div>
  )
}

app/@modal/default.js:

export default function Default() {
  return null
}

Comportamiento

  1. Clic en inicio: navegación cliente a /photos/1 → interceptada → modal sobre el feed; URL actualizada sin recarga total.
  2. Atrás: router.back()/@modal renderiza default.js → modal desaparece.
  3. Recarga o /photos/1 directo: sin interceptación → photos/[id]/page.js completo.
  4. Enlace compartido: página completa para quien abre la URL.

URL compartible, atrás cierra modal, recarga muestra página entera.

Elegir el nivel de interceptación

Con interceptación en app/@modal/:

  • Objetivo app/photos/ (mismo nivel) → (.)photos
  • Objetivo app/shop/products/(..)
  • Cualquier profundidad → (...)

Ejemplo:

app/
└── shop/
    ├── @modal/
    │   └── (..)products/
    │       └── [id]/
    └── products/
        └── [id]/

@modal está en shop/; products es hijo de shop(..).

Consejo: empieza con (...); cuando funcione, afina a (.) o (..).

Tres trampas

Trampa 1: sin default.js el modal no se cierra

Sin default.js, el slot conserva el modal. Añade default.js con null.

Trampa 2: useRouter en Server Component

El modal suele usar useRouter().back()'use client' obligatorio.

Trampa 3: anidación profunda

Con rutas como app/shop/(store)/products/[id], calcula bien la ruta. Si te pierdes, (...) desde la raíz es menos elegante pero seguro.


Interceptadas + paralelas cubren modales con URL compartible, recarga con página completa, atrás cierra y adelante reabre. Instagram, Twitter y Airbnb usan este patrón; en Next.js también puedes.

Práctica integrada: las cuatro técnicas juntas

Escenario: e-commerce completo

La fuerza está en combinarlas. Requisitos de un e-commerce mediano:

Tres zonas (layouts distintos):

  • Marketing (/, /about, /pricing): hero + nav simple
  • Tienda (/products, /cart): carrito fijo + categorías
  • Panel (/dashboard): sidebar + permisos

Tienda:

  • Listado con filtros laterales
  • Detalle con migas de pan
  • Clic en tarjeta → vista rápida en modal sin salir del listado
  • Recarga o URL directa → detalle completo

Panel:

  • Ventas, pedidos e inventario en paralelo, cada uno con loading y error propios

Estructura de directorios

app/
├── layout.js

├── (marketing)/
│   ├── layout.js
│   ├── page.js
│   ├── about/
│   └── pricing/

├── (shop)/
│   ├── layout.js
│   ├── @modal/
│   │   ├── (.)products/
│   │   │   └── [id]/
│   │   │       └── page.js
│   │   └── default.js
│   │
│   ├── products/
│   │   ├── layout.js
│   │   ├── page.js
│   │   └── [id]/
│   │       ├── layout.js
│   │       └── page.js
│   │
│   └── cart/
│       └── page.js

└── (dashboard)/
    ├── layout.js
    ├── @sales/
    │   ├── page.js
    │   └── loading.js
    ├── @orders/
    │   ├── page.js
    │   └── loading.js
    ├── @inventory/
    │   └── page.js
    └── page.js
  • Grupos separan las tres zonas
  • Layouts anidados añaden UI en tienda
  • Paralelas: modal en tienda, módulos en panel
  • Interceptadas: vista rápida de producto

Código clave

app/(shop)/layout.js:

export default function ShopLayout({ children, modal }) {
  return (
    <div>
      <nav>{/* Carrito + categorías */}</nav>
      {children}
      {modal}
    </div>
  )
}

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

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

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

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>
  )
}

Clic → navegación cliente → interceptación → modal.

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>Vista rápida del producto</h2>
        <ProductPreview id={params.id} />
        <Link href={`/products/${params.id}`} onClick={() => router.back()}>
          Ver detalle completo
        </Link>
      </div>
    </div>
  )
}

app/(dashboard)/layout.js:

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

Por qué esta arquitectura

Grupos de rutas: nav distinta por zona; equipos separados, menos conflictos.

Layouts anidados: filtros en listado, migas en detalle, nav de tienda compartida; carpetas = capas de UI.

Interceptadas + paralelas en modales: vista rápida sin abandonar el listado; URL /products/123 para compartir y SEO; recarga = página completa.

Paralelas en panel: APIs y tiempos distintos; fallos aislados por módulo.

Beneficios en equipo

Tras la reestructuración:

  • Conflictos −65 %: frontend en (shop), backend en (dashboard)
  • Onboarding más rápido: el árbol de carpetas explica el dominio
  • Mantenimiento: un layout.js por zona, sin efectos colaterales
  • Conversión +23 % en vista rápida vs. salto a detalle (menos fricción al volver)

Estas cuatro técnicas no son postureo: estructura más clara, equipo más fluido, UX mejor. Proyectos pequeños no hace falta forzarlas; con decenas de rutas, varios equipos y modales complejos, este esquema ayuda de verdad.

Conclusión

El proyecto de 60 carpetas desordenadas, tras reestructurar, dejó de consumir tiempo en buscar archivos, resolver conflictos y pelear con layouts. Puedes centrarte en la lógica de negocio.

Resumen:

TécnicaFunciónCuándo usarlaSintaxis
Grupos de rutasOrganizar archivos, aislar layoutsVarias zonas, varios equipos(folderName)
Layouts anidadosApilar UI por capasNav multinivellayout.js en cada nivel
Rutas paralelasVarios fragmentos a la vezPanel, módulos independientes@folderName
Rutas interceptadasInterceptar nav, modalesModales estilo Instagram(.) (..) (...)

No uses todo de golpe. Empieza por grupos de rutas; layouts anidados cuando la UI se apila; paralelas e interceptadas para panel y modales.

La primera lectura de la documentación confunde; después de usarlas una vez, encajan: carpetas = rutas, jerarquía de archivos = jerarquía de UI, interceptación = experiencia.

Si tu Next.js ya se siente abultado, dedica medio día a reagrupar rutas. En tres meses probablemente te lo agradezcas.

Flujo completo de reestructuración con rutas avanzadas de Next.js

Pasa de una estructura caótica a una arquitectura clara con grupos de rutas, layouts anidados, rutas paralelas e interceptadas

⏱️ Estimated time: 4 hr

  1. 1

    Step 1: Analizar la estructura actual del proyecto

    Evalúa la cantidad y complejidad de rutas:
    • Cuenta carpetas bajo app (más de 20 → conviene grupos de rutas)
    • Identifica zonas funcionales (marketing, tienda, panel, etc.)
    • Localiza grupos de páginas que necesitan layouts distintos
    • Registra los puntos de conflicto del equipo

    Criterios:
    • Más de 20 carpetas: grupos de rutas
    • Navegación multinivel: layouts anidados
    • Varios módulos independientes a la vez: rutas paralelas
    • Interacción con modales: rutas interceptadas + paralelas
  2. 2

    Step 2: Crear grupos de rutas por zona funcional

    Usa paréntesis para agrupar por función o equipo:
    • Grupo (marketing): inicio, about, pricing
    • Grupo (shop): productos, carrito
    • Grupo (dashboard): pedidos, usuarios

    Notas:
    • El nombre del grupo no afecta la URL, pero la misma URL no puede estar en varios grupos
    • Cada grupo puede tener su layout.js
    • page.js de inicio debe ir dentro de un grupo (no en app/page.js)
  3. 3

    Step 3: Diseñar la jerarquía de layouts anidados

    Diseña según la jerarquía de UI:
    • Nivel 1: app/layout.js (global: Header + Footer)
    • Nivel 2: layout.js de zona (p. ej. blog/layout.js con sidebar)
    • Nivel 3: layout.js de detalle (p. ej. blog/[slug]/layout.js con índice)

    Puntos clave:
    • Cada layout solo añade UI propia de ese nivel
    • Los layouts hijos heredan del padre
    • El layout no se vuelve a renderizar; buen rendimiento
  4. 4

    Step 4: Implementar rutas paralelas (si hace falta)

    Crea slots con @:
    • @modal: para modales
    • @sales, @orders: módulos independientes del panel

    Recíbelos en layout.js:
    • export default function Layout({ children, modal, sales, orders })
    • En JSX: {modal} {sales} {orders}

    Cada slot puede tener su loading.js y error.js
  5. 5

    Step 5: Implementar rutas interceptadas (si necesitas modales)

    Crea la estructura de interceptación:
    • En @modal: (.)photos/[id]/page.js (intercepta ruta del mismo nivel)
    • photos/[id]/page.js (página completa)

    Sintaxis:
    • (.): intercepta rutas del mismo nivel
    • (..): intercepta el nivel superior
    • (...): intercepta desde la raíz

    Debes crear default.js que devuelva null para poder cerrar el modal
  6. 6

    Step 6: Probar y validar

    Comprueba todo:
    • Grupos de rutas: URL sin cambios, estructura clara
    • Layouts anidados: jerarquía de UI correcta, estado conservado
    • Rutas paralelas: carga independiente, errores aislados
    • Rutas interceptadas: modal en navegación cliente, página completa al recargar

    Rendimiento:
    • Usa Next.js DevTools para contar renders de layout
    • Confirma que el layout no se vuelve a renderizar al cambiar subrutas

FAQ

¿Los grupos de rutas afectan la URL?
No. Se nombran con paréntesis (p. ej. (marketing)); Next.js ignora el nombre y la URL no cambia. Por ejemplo, (marketing)/about/page.js sigue siendo /about, no /marketing/about.
¿Cuándo debería usar grupos de rutas?
Cuando hay más de 20 carpetas bajo app o necesitas layouts raíz distintos por zona funcional. Encajan muy bien en proyectos con varios equipos y reducen conflictos en Git.
¿Los layouts anidados afectan el rendimiento?
No. Al cambiar subrutas, los layouts anidados no se vuelven a renderizar; solo se recarga el page.js más interno. Puedes conservar estado en el layout (scroll del sidebar, contenido del buscador, etc.).
¿En qué se diferencian las rutas paralelas de un componente normal?
Cada slot es un fragmento de página independiente con su loading.js y error.js, cargando en paralelo. Un componente normal espera a que carguen todos los datos y un fallo puede tumbar toda la página.
¿Cómo elegir la sintaxis (.) (..) (...) de rutas interceptadas?
(.) intercepta rutas del mismo nivel (p. ej. @modal y photos bajo app), (..) el nivel superior y (...) desde la raíz. Si dudas, prueba primero (...) y ajusta según la estructura de carpetas.
¿Por qué las rutas interceptadas necesitan default.js?
Sin default.js, cuando la ruta no coincide el slot conserva el contenido anterior (el modal sigue visible). default.js debe devolver null para cerrar el modal correctamente.
¿Hay que reestructurar rutas al migrar de Pages Router a App Router?
No siempre. Proyectos pequeños (menos de 20 carpetas) pueden mantener estructura plana. En proyectos grandes o con layouts e interacciones complejas, grupos de rutas y layouts anidados mejoran mucho la mantenibilidad.

15 min de lectura · Publicado el: 18 dic 2025 · Actualizado el: 21 ago 2026

Comentarios

Inicia sesión con GitHub para dejar un comentario

Easton BlogEaston Blog