Next.js
App Router y routing basado en archivos
Explica cómo el App Router convierte carpetas y archivos especiales en rutas, layouts, boundaries, endpoints y segmentos dinámicos de Next.js.
- Última actualización
- Actualizada
- Nivel
- Fundamentos
Next.js
Explica cómo el App Router convierte carpetas y archivos especiales en rutas, layouts, boundaries, endpoints y segmentos dinámicos de Next.js.
El App Router convierte la estructura del directorio app en un árbol de segmentos, layouts, páginas y boundaries. Las carpetas describen jerarquía; los archivos especiales determinan qué comportamiento aporta cada segmento. No todo archivo dentro de app crea una URL pública.
El routing basado en archivos establece una relación visible entre la URL y el árbol de interfaz:
URL /dashboard/orders/42
↓
app/
layout.tsx
dashboard/
layout.tsx
orders/
[orderId]/
page.tsxPara esa URL, Next.js compone aproximadamente:
RootLayout
└─ DashboardLayout
└─ OrderPageCada carpeta normal representa un segmento potencial. La ruta solo se vuelve pública cuando existe una convención que la expone, principalmente page.tsx para UI o route.ts para una respuesta HTTP.
Un router necesita relacionar:
En un router configurado manualmente, esa relación puede vivir en un objeto central:
const routes = [
{ path: "/dashboard", element: <Dashboard /> },
{ path: "/dashboard/orders/:id", element: <Order /> },
];El App Router distribuye la configuración en el árbol de archivos. Esto facilita colocation y composición, pero exige conocer las convenciones reservadas.
folder name
→ define posición dentro del árbol de segmentos
special file
→ añade una responsabilidad al segmento
page or route
→ hace pública una entrada
layout and boundaries
→ envuelven descendientesUna carpeta no equivale automáticamente a una página. Esto permite colocar componentes, schemas y tests cerca de una ruta sin exponerlos.
Considera:
app/
├─ layout.tsx
├─ page.tsx
├─ dashboard/
│ ├─ layout.tsx
│ ├─ page.tsx
│ └─ orders/
│ ├─ page.tsx
│ └─ [orderId]/
│ └─ page.tsx
└─ api/
└─ health/
└─ route.tsRutas resultantes:
app/page.tsx → /
app/dashboard/page.tsx → /dashboard
app/dashboard/orders/page.tsx → /dashboard/orders
app/dashboard/orders/[orderId]/page → /dashboard/orders/:orderId
app/api/health/route.ts → /api/healthlayout.tsx no crea una URL. Participa cuando uno de sus descendientes públicos coincide.
Dentro de un segmento, Next.js coordina los archivos en una jerarquía conceptual:
layout
└─ template
└─ error boundary
└─ loading / Suspense boundary
└─ not-found handling
└─ page o layout descendienteLa implementación exacta pertenece al framework, pero el modelo ayuda a decidir el alcance de cada fallback o fallo.
Define la UI pública de una ruta:
// app/products/page.tsx
export default function ProductsPage() {
return <h1>Productos</h1>;
}Debe exportar un componente por defecto. En App Router es Server Component por defecto.
Una page es la hoja del árbol que corresponde al contenido específico de esa URL.
Comparte UI con un segmento y sus descendientes:
// app/dashboard/layout.tsx
import type { ReactNode } from "react";
export default function DashboardLayout({ children }: { children: ReactNode }) {
return (
<div className="dashboard-shell">
<DashboardNavigation />
<main>{children}</main>
</div>
);
}Los layouts se preservan durante navegaciones compatibles. Son apropiados para shell, navegación y providers con alcance de una sección.
Tiene una API similar al layout, pero obtiene una instancia nueva durante ciertas navegaciones. Sirve cuando necesitas reiniciar estado o Effects de esa estructura.
No lo uses simplemente porque quieres compartir markup; para eso existe layout.
Crea una Suspense boundary asociada al segmento:
export default function Loading() {
return <OrdersSkeleton />;
}Permite mostrar una shell inmediatamente mientras contenido descendiente espera.
Define recuperación para errores inesperados dentro del segmento:
"use client";
export default function ErrorPage({
error,
unstable_retry,
}: {
error: Error;
unstable_retry: () => void;
}) {
return (
<section>
<h2>No fue posible cargar esta sección</h2>
<button onClick={unstable_retry}>Intentar de nuevo</button>
</section>
);
}En Next.js 16.2, unstable_retry está disponible como capacidad experimental para volver a solicitar datos además de reiniciar la boundary. Una nota estable debe distinguirlo del reset tradicional.
Representa recursos inexistentes cuando se llama notFound() o según el flujo del router.
No debe usarse para ocultar errores de red, fallos de base de datos o permisos.
Define un Route Handler mediante Web Request y Response APIs:
// app/api/health/route.ts
export function GET() {
return Response.json({ status: "ok" });
}No puedes tener page.tsx y route.ts resolviendo el mismo segmento final porque ambas convenciones compiten por la misma ruta.
Proporciona fallback para slots de Parallel Routes durante una carga completa cuando Next.js no conoce su estado activo anterior.
El App Router también reconoce:
global-error.tsx para fallos fuera de boundaries normales.forbidden.tsx y unauthorized.tsx bajo APIs compatibles.proxy.ts en la raíz o src para ejecutar lógica previa al routing según la versión actual.instrumentation.ts para inicialización y observabilidad.robots.ts, sitemap.ts, icon.png y opengraph-image.tsx.No todos pertenecen dentro de cada segmento; revisa su ubicación admitida.
Puedes colocar archivos comunes junto a la ruta:
app/products/
├─ page.tsx
├─ product-list.tsx
├─ product-list.test.tsx
├─ schema.ts
└─ queries.tsSolo page.tsx crea /products. Los demás son módulos normales.
La feature puede cambiar en un solo lugar.
Si colocas toda la lógica bajo app, otros contextos pueden depender accidentalmente del router. Un repositorio o caso de uso reutilizable puede vivir en features o server.
app/dashboard/
├─ _components/
├─ _lib/
└─ page.tsxEl prefijo _ excluye la carpeta y sus descendientes del routing.
No es obligatorio para colocation, porque un archivo común ya no crea rutas. Aporta una frontera explícita y evita posibles conflictos con futuras convenciones.
Para crear un segmento público que realmente empiece con underscore, se utiliza su forma codificada %5F.
app/
├─ (marketing)/
│ ├─ layout.tsx
│ ├─ page.tsx
│ └─ pricing/page.tsx
└─ (app)/
└─ dashboard/page.tsxLos paréntesis no aparecen en la URL:
(marketing)/pricing → /pricing
(app)/dashboard → /dashboardSirven para organizar, aplicar layouts a subconjuntos o definir múltiples root layouts. Dos grupos no pueden producir la misma URL final.
[slug] → un segmento
[...parts] → uno o más segmentos
[[...parts]] → cero o más segmentosEjemplo:
// app/products/[slug]/page.tsx
type ProductPageProps = {
params: Promise<{ slug: string }>;
};
export default async function ProductPage({ params }: ProductPageProps) {
const { slug } = await params;
return <h1>Producto: {slug}</h1>;
}params es una Promise en el modelo actual. Su tipo describe la forma, pero no valida el valor proveniente de la URL.
Una carpeta con @ define un slot, no un segmento URL:
app/dashboard/
├─ @analytics/page.tsx
├─ @activity/page.tsx
└─ layout.tsxEl layout recibe props para los slots:
export default function DashboardLayout({
children,
analytics,
activity,
}: {
children: React.ReactNode;
analytics: React.ReactNode;
activity: React.ReactNode;
}) {
return (
<>
{children}
{analytics}
{activity}
</>
);
}Los slots permiten subestados navegables independientes. Añaden complejidad de hard navigation, defaults y recuperación.
Patrones como (.)photo o (..)photo permiten mostrar otra ruta dentro del contexto actual:
feed → click /photo/42 → modal sobre feed
refresh /photo/42 → página completaLa profundidad se calcula por segmentos de ruta, no por carpetas físicas como @modal.
app/
└─ dashboard/
├─ layout.tsx
├─ loading.tsx
├─ error.tsx
├─ page.tsx
└─ orders/
├─ page.tsx
└─ [orderId]/
├─ page.tsx
└─ not-found.tsxdashboard, orders y [orderId].params.orderId entra a la page dinámica.notFound() y usa el fallback más cercano.dashboard/loading.tsx puede aparecer según la boundary.dashboard/error.tsx sin desmontar necesariamente toda la aplicación.Una carpeta (protected) o un layout que redirige no protege por sí solo datos y mutaciones.
UI guard
→ mejora navegación
Data Access Layer
→ verifica que el usuario pueda leer la entidad
Server Action / Route Handler
→ valida sesión, input y permiso de la operaciónLa URL es entrada no confiable. Un atacante puede llamar directamente una Action o endpoint.
app/(a)/settings/page.tsx
app/(b)/settings/page.tsxAmbas intentan crear /settings; el build debe fallar o exigir reorganización.
Representan respuestas diferentes para el mismo pathname. Separa el endpoint bajo otro segmento, normalmente api.
Un catch-all puede absorber URLs inesperadas. Valida segmentos y define not-found explícito.
Un archivo llamado page.tsx tiene significado especial. Si solo es un componente interno, utiliza otro nombre o una carpeta privada.
pages._app y _document para wrappers globales.getStaticProps y getServerSideProps.pages/api.Ambos pueden coexistir durante migración, pero no deben resolver la misma URL.
Solo las convenciones públicas la crean. Revisa la estructura del build si aparece una URL inesperada.
El dominio puede necesitar módulos fuera del router. No obligues a que cada dependencia sea descendiente de app.
Añaden profundidad sin aportar layout o frontera. Cada grupo debe tener una razón.
Puede ampliar trabajo y dependencias a rutas que no lo necesitan.
Si no necesitas deep link, back navigation ni carga directa, state local es más simple.
Parallel e intercepting routes pueden comportarse distinto al recargar.
next build y revisa la clasificación de rutas.page y route hacen pública una entrada.app sin ser rutas.app/products/card.tsx no crea /products/card?page.tsx y route.ts en el mismo segmento?_components si la colocation ya es segura?page o route expone el segmento.Pages, layouts y templates profundiza en cómo se compone, preserva o reinicia la UI compartida durante navegación.