Entendiendo la estructura del App Router de Next.js: app/, layouts, pages, loading y error
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/:slugEn 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.tsxCó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
- Mantene el root layout lo más ligero posible: solo incluí lo que es realmente global (etiquetas , , proveedores de contexto, estilos globales).
- Aprovechá los layouts anidados para evitar repetir UI: si varias rutas comparten una barra lateral o un header, ponelo en un layout común.
- Usá loading.js solo cuando realmente tengas una demora notable: si el segmento se renderiza rápido, el skeleton puede generar un parpadeo innecesario.
- 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).
- 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. - 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.
- Aprovechá el metadata exportado: definí título y descripción en cada layout o page para mejorar el SEO y el compartir en redes.
- 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.
DUGLAS MORENO
Comentarios (0)
Sé el primero en comentar.