Entendiendo la estructura del App Router de Next.js: app/, layouts, pages, loading y error

DUGLAS MORENODUGLAS MORENO 👁 8

Introducción

El App Router de Next.js es la nueva forma de definir rutas y componentes que llegó con la versión 13 y se consolidó en la 14. Si tenés experiencia con el antiguo pages router, notarás que ahora la convención cambia: todo vive dentro de la carpeta app/ y se basa en conceptos de layout, page, loading y error. Este artículo te va a guiar paso a paso por cada pieza, mostrando cómo se relacionan y qué salida esperás ver en el navegador. Al final vas a poder estructurar una aplicación Next.js con el App Router de forma clara, siguiendo buenas prácticas y evitando los errores más comunes.

¿Qué es el App Router y por qué usarlo

El App Router aprovecha las características de React 18, como los Server Components y Streaming, para ofrecer mejor rendimiento y una experiencia de desarrollo más intuitiva. En vez de mezclar lógica de cliente y servidor en el mismo archivo, podés decidir explícitamente qué partes se renderizan en el servidor y cuáles en el cliente. Además, el sistema de rutas se basa en el sistema de archivos: cada carpeta dentro de app/ representa un segmento de la URL, y los archivos especiales como layout.js, page.js, loading.js y error.js tienen un rol bien definido.

A quién está dirigido

Este post está pensado para desarrolladores que ya conocen los fundamentos de React y que quieran pasar al App Router de Next.js, ya sea porque están empezando un nuevo proyecto o porque quieren migrar una aplicación existente. También sirve como referencia rápida para quien necesita recordar la convención de archivos y el comportamiento de cada uno.

Qué vas a aprender

  • Cómo está organizada la carpeta app/ y qué significa cada nivel.
  • Qué hace cada archivo especial: layout, page, loading y error.
  • Cómo combinar layouts anidados para reutilizar UI.
  • Qué salida produce el servidor y el cliente en cada caso.
  • Buenas prácticas para mantener el código limpio y escalable.
  • Errores frecuentes y cómo evitarlos.

Organización de la carpeta app/

La raíz del proyecto contiene la carpeta app/. Dentro de ella, cada subcarpeta representa un segmento de ruta. Por ejemplo:

app/
├── layout.js          ← layout raíz (aplica a toda la app)
├── page.js            ← página de la ruta raíz (/)
├── dashboard/
│   ├── layout.js      ← layout del segmento /dashboard
│   ├── page.js        ← página /dashboard
│   └── analytics/
│       ├── layout.js  ← layout de /dashboard/analytics
│       └── page.js    ← página /dashboard/analytics
└── blog/
    ├── layout.js
    ├── page.js        ← página /blog
    └── [slug]/
        ├── layout.js
        └── page.js    ← página /blog/:slug

En este árbol:

  • layout.js define la UI que envuelve a todos los hijos de ese segmento.
  • page.js es el componente que se muestra cuando la ruta coincide exactamente con ese segmento.
  • Los segmentos pueden ser dinámicos usando corchetes, como [slug].

Layouts: composición y reutilización

Un layout es un componente que recibe una prop llamada children y la renderiza dentro de su UI. El layout más importante es el root layout (app/layout.js), que debe incluir las etiquetas y . Los layouts hijos no necesitan volver a declarar esas etiquetas; simplemente envuelven a sus hijos.

Ejemplo de root layout

// app/layout.tsx
export const metadata = {
  title: 'Mi App Next',
  description: 'Una app de ejemplo con App Router',
};

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang='es'>
      <body>
        <nav>
          <ul>
            <li><a href='/'>Home</a></li>
            <li><a href='/dashboard'>Dashboard</a></li>
            <li><a href='/blog'>Blog</a></li>
          </ul>
        </nav>
        {children}
      </body>
    </html>
  );
}

En este ejemplo usamos metadata para definir el título y la descripción que Next.js pone en el . Notá que no hay comillas dobles dentro del JSX; usamos comillas simples para los atributos.

Layout de segmento

// app/dashboard/layout.tsx
export default function DashboardLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <section style={{ padding: '2rem' }}>
      <h1>Dashboard</h1>
      {children}
    </section>
  );
}

Este layout se aplica a todas las rutas bajo /dashboard, como /dashboard y /dashboard/analytics.

Páginas: el contenido final

El archivo page.js (o page.tsx) es el que se renderiza cuando la URL coincide exactamente con el segmento. No recibe ninguna prop especial; su salida reemplaza a children en el layout más cercano.

Página de inicio

// app/page.tsx
export default function HomePage() {
  return (
    <main>
      <h2>Bienvenido a la página de inicio</h2>
      <p>Esta es la ruta raíz (/).</p>
    </main>
  );
}

Página del dashboard

// app/dashboard/page.tsx
export default function DashboardPage() {
  return (
    <article>
      <h2>Panel de control</h2>
      <p>Acá vas a ver tus métricas.</p>
    </article>
  );
}

Página dinámica del blog

// app/blog/[slug]/page.tsx
export default function BlogPostPage({
  params,
}: {
  params: { slug: string };
}) {
  return (
    <article>
      <h2>Post: {params.slug}</h2>
      <p>Contenido del post {params.slug}.</p>
    </article>
  );
}

loading.js y error.js: manejo de estados

Next.js permite definir dos archivos especiales que mejoran la experiencia de usuario durante la carga de datos y cuando ocurre un error.

loading.js

Este archivo define un componente de esqueleto (skeleton) que se muestra mientras el segmento correspondiente está cargando sus datos o sus componentes hijos. Se usa principalmente con Server Components que hacen fetch o con Suspense.

// app/dashboard/loading.tsx
export default function DashboardLoading() {
  return (
    <div style={{ padding: '2rem', color: '#666' }}>
      Cargando dashboard…
    </div>
  );
}

Cuando navegás a /dashboard, si el layout o la página tardan en resolverse, verás este mensaje hasta que el contenido esté listo.

error.js

Este componente captura errores que ocurren en su segmento o en cualquiera de sus hijos. Recibe dos props: error (el objeto Error) y reset (una función que permite intentar volver a renderizar el segmento).

// app/dashboard/error.tsx
export default function DashboardError({
  error,
  reset,
}: {
  error: Error;
  reset: () => void;
}) {
  return (
    <div style={{ padding: '2rem', color: '#c00' }}>
      <h2>Ocurrió un error</h2>
      <p>{error.message}</p>
      <button onClick={() => reset()}>Intentar de nuevo</button>
    </div>
  );
}

Si algún fetch dentro de DashboardPage lanza una excepción, este componente se mostrará en lugar de romper toda la UI.

Árbol de ejemplo con salida esperada

Vamos a poner todo junto en un pequeño proyecto para ver cómo se combina la UI. Supongamos la siguiente estructura:

app/
├── layout.tsx
├── page.tsx
├── dashboard/
│   ├── layout.tsx
│   ├── page.tsx
│   ├── loading.tsx
│   └── error.tsx
└── blog/
    ├── layout.tsx
    ├── page.tsx
    └── [slug]/
        ├── layout.tsx
        └── page.tsx

Código de cada archivo

app/layout.tsx (root layout) – ya mostrado arriba.

app/page.tsx (página de inicio) – ya mostrado arriba.

app/dashboard/layout.tsx – layout del segmento dashboard.

app/dashboard/page.tsx – página del dashboard.

app/dashboard/loading.tsx – esqueleto de carga.

app/dashboard/error.tsx – manejo de error.

app/blog/layout.tsx – layout del blog (opcional, pero lo incluimos).

// app/blog/layout.tsx
export default function BlogLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <section style={{ padding: '1.5rem', backgroundColor: '#f9f9f9' }}>
      <h1>Blog</h1>
      {children}
    </section>
  );
}

app/blog/page.tsx – lista de posts (simulada).

// app/blog/page.tsx
export default function BlogListPage() {
  const posts = ['primer-post', 'segundo-post', 'tercer-post'];
  return (
    <div>
      <h2>Lista de posts</h2>
      <ul>
        {posts.map((p) => (
          <li key={p}>
            <a href={`/blog/${p}`}>Ver {p}</a>
          </li>
        ))}
      </ul>
    </div>
  );
}

app/blog/[slug]/layout.tsx – layout opcional para cada post.

// app/blog/[slug]/layout.tsx
export default function PostLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <article style={{ border: '1px solid #ddd', padding: '1rem', margin: '1rem 0' }}>
      {children}
    </article>
  );
}

app/blog/[slug]/page.tsx – página del post individual (ya mostrada).

Resultado en el navegador

Al visitar las distintas rutas, verías lo siguiente:

  • / (raíz):

    • El root layout muestra el
    • Dentro de , el componente HomePage aparece.
    • Salida visual:
      [ Home ][ Dashboard ][ Blog ]
      Bienvenido a la página de inicio
      Esta es la ruta raíz (/).
  • /dashboard:

    • Se aplica root layout, luego DashboardLayout (porque coincide con el segmento dashboard).
    • Dentro de DashboardLayout se renderiza DashboardPage.
    • Mientras carga, si hubiere un loading, se vería el mensaje de DashboardLoading.
    • Si ocurre un error, se muestra DashboardError con el botón de reintento.
    • Salida esperada (sin carga ni error):
      [ Home ][ Dashboard ][ Blog ]
      Dashboard
      Panel de control
      Acá vas a ver tus métricas.
  • /dashboard/analytics (si existiera una página analytics/page.tsx):

    • Se aplican root layout → DashboardLayout → analytics/layout (si existe) → analytics/page.
    • El layout de dashboard sigue estando presente, brindando consistencia.
  • /blog:

    • Root layout → BlogLayout → BlogListPage.
    • Verías el título "Blog" y la lista de enlaces a cada post.
  • /blog/primer-post:

    • Root layout → BlogLayout → PostLayout (del segmento [slug]) → BlogPostPage.
    • El PostLayout agrega un borde y padding alrededor del contenido.
    • Salida:
      [ Home ][ Dashboard ][ Blog ]
      Blog
      Post: primer-post
      Contenido del post primer-post.

Cómo se combina el streaming

Si alguna de tus páginas hace una petición lenta (por ejemplo, fetch a una API externa), Next.js puede enviar el HTML de los layouts y del skeleton inmediatamente, y luego ir reemplazando el skeleton con el contenido real a medida que llega. Esto se logra gracias a Suspense y a los Server Components. El loading.js actúa como el fallback de ese Suspense implícito.

Buenas prácticas

  1. Mantene el root layout lo más ligero posible: solo incluí lo que es realmente global (etiquetas , , proveedores de contexto, estilos globales).
  2. Aprovechá los layouts anidados para evitar repetir UI: si varias rutas comparten una barra lateral o un header, ponelo en un layout común.
  3. Usá loading.js solo cuando realmente tengas una demora notable: si el segmento se renderiza rápido, el skeleton puede generar un parpadeo innecesario.
  4. Manejá errores con granularidad: poné un error.js en cada segmento donde quiera darle al usuario una posibilidad de recuperación específica (por ejemplo, intentar cargar de nuevo un dashboard).
  5. Preferí Server Components por defecto: dejá los componentes interactivos (que usen estado, efectos o navegación) como Client Components, marcándolos con 'use client' en la parte superior del archivo.
  6. No mezcles lógica de datos en los layouts: los layouts deben ser puros en cuanto a datos; si necesitás fetch, hacelo en la page o en un componente hijo que lo requiera.
  7. Aprovechá el metadata exportado: definí título y descripción en cada layout o page para mejorar el SEO y el compartir en redes.
  8. Usá rutas dinámicas con responsabilidad: evité crear demasiados segmentos dinámicos innecesarios, ya que cada uno genera una nueva combinación de layouts y pages que puede aumentar el tamaño del bundle si no se cuida.

Errores comunes

  • Olvidar exportar el componente como default: si el archivo no tiene una export default, Next.js lanzará un error diciendo que no pudo encontrar el componente de página o layout.
  • Poner comillas dobles en JSX sin escaparlas: aunque en JSX es válido, si estás escribiendo dentro de un string de JSON o en ciertos contextos puede romperse; por eso recomendamos usar comillas simples para atributos.
  • Colocar page.js dentro de una carpeta que también tiene un layout.js con el mismo nombre: esto genera confusión; recordá que page.js corresponde a la ruta exacta de esa carpeta, mientras que layout.js envuelve a sus hijos.
  • Usar useEffect o useState en un Server Component sin marcarlo como 'use client': esto provocará un error en tiempo de compilación porque los Server Components no pueden usar hooks de cliente.
  • Esperar que loading.js se muestre en navegaciones cliente a cliente: loading.js solo funciona para transiciones que involucren Server Components o Suspense; si navegás entre dos Client Components puramente, no verás el skeleton.
  • No definir un root layout: si faltan app/layout.js, Next.js usará un layout interno pero vos perdés la oportunidad de agregar metadata o etiquetas personalizadas.

Conclusión

El App Router de Next.js cambia la forma de pensar las rutas: ya no es solo un archivo por URL, sino una jerarquía de layouts y pages que permite reutilizar UI, manejar estados de carga y error de forma declarativa, y aprovechar al máximo las capacidades de Server Components y Streaming de React 18. Con la estructura de carpetas bien definida y los archivos especiales (layout, page, loading, error) claros, podés construir aplicaciones escalables y mantenibles sin perder rendimiento.

Ahora que ya conocés cada pieza, te animo a probar creando un pequeño proyecto con las rutas que mostramos aquí, experimentando con loading y error, y viendo cómo el HTML se envía en streaming. La práctica constante es la mejor forma de interiorizar estas ideas y llevarlas a tus aplicaciones reales.

Si tenés alguna duda o querés profundizar en algún punto (por ejemplo, cómo combinar el App Router con rutas de API o cómo usar el nuevo generateMetadata), dejá un comentario y seguimos la conversación.

Comentarios (0)

Sé el primero en comentar.

Dejá tu comentario