Español

Desbloquea el poder del App Router de Next.js con nuestra guía detallada sobre el enrutamiento basado en archivos. Aprende a estructurar tu aplicación, crear rutas dinámicas, gestionar layouts y más.

App Router de Next.js: Una Guía Completa sobre el Enrutamiento Basado en Archivos

El App Router de Next.js, introducido en Next.js 13 y convirtiéndose en el estándar en versiones posteriores, revoluciona cómo estructuramos y navegamos por las aplicaciones. Introduce un sistema de enrutamiento basado en archivos potente e intuitivo que simplifica el desarrollo, mejora el rendimiento y potencia la experiencia general del desarrollador. Esta guía completa profundizará en el enrutamiento basado en archivos del App Router, proporcionándote el conocimiento y las habilidades para construir aplicaciones de Next.js robustas y escalables.

¿Qué es el Enrutamiento Basado en Archivos?

El enrutamiento basado en archivos es un sistema de enrutamiento donde la estructura de las rutas de tu aplicación está directamente determinada por la organización de tus archivos y directorios. En el App Router de Next.js, defines las rutas creando archivos dentro del directorio `app`. Cada carpeta representa un segmento de ruta, y archivos especiales dentro de esas carpetas definen cómo se manejará ese segmento de ruta. Este enfoque ofrece varias ventajas:

Primeros Pasos con el App Router

Para usar el App Router, necesitas crear un nuevo proyecto de Next.js o migrar un proyecto existente. Asegúrate de estar usando la versión 13 de Next.js o posterior.

Creando un Nuevo Proyecto:

Puedes crear un nuevo proyecto de Next.js con el App Router usando el siguiente comando:

npx create-next-app@latest my-app --example with-app

Migrando un Proyecto Existente:

Para migrar un proyecto existente, necesitas mover tus páginas del directorio `pages` al directorio `app`. Puede que necesites ajustar tu lógica de enrutamiento en consecuencia. Next.js proporciona una guía de migración para ayudarte con este proceso.

Conceptos Fundamentales del Enrutamiento Basado en Archivos

El App Router introduce varios archivos y convenciones especiales que definen cómo se manejan tus rutas:

1. El Directorio `app`

El directorio `app` es la raíz de las rutas de tu aplicación. Todos los archivos y carpetas dentro de este directorio se usarán para generar rutas. Cualquier cosa fuera del directorio `app` (como el directorio `pages` si estás migrando) será ignorada por el App Router.

2. El Archivo `page.js`

El archivo `page.js` (o `page.jsx`, `page.ts`, `page.tsx`) es la parte más fundamental del App Router. Define el componente de UI que se renderizará para un segmento de ruta específico. Es un archivo **obligatorio** para cualquier segmento de ruta al que quieras que se pueda acceder directamente.

Ejemplo:

Si tienes una estructura de archivos como esta:

app/
  about/
    page.js

El componente exportado desde `app/about/page.js` se renderizará cuando un usuario navegue a `/about`.

// app/about/page.js
import React from 'react';

export default function AboutPage() {
  return (
    <div>
      <h1>Sobre Nosotros</h1>
      <p>Aprende más sobre nuestra compañía.</p>
    </div>
  );
}

3. El Archivo `layout.js`

El archivo `layout.js` (o `layout.jsx`, `layout.ts`, `layout.tsx`) define una UI que se comparte entre múltiples páginas dentro de un segmento de ruta. Los layouts son útiles para crear encabezados, pies de página, barras laterales y otros elementos consistentes que deben estar presentes en múltiples páginas.

Ejemplo:

Digamos que quieres añadir un encabezado tanto a la página `/about` como a una hipotética página `/about/team`. Puedes crear un archivo `layout.js` en el directorio `app/about`:

// app/about/layout.js
import React from 'react';

export default function AboutLayout({ children }) {
  return (
    <div>
      <header>
        <h1>Acerca de Nuestra Compañía</h1>
      </header>
      <main>{children}</main>
    </div>
  );
}

La prop `children` será reemplazada con la UI renderizada por el archivo `page.js` en el mismo directorio o en cualquier directorio anidado.

4. El Archivo `template.js`

El archivo `template.js` es similar a `layout.js`, pero crea una nueva instancia del componente para cada ruta hija. Esto es útil para escenarios donde quieres mantener el estado del componente o evitar re-renderizaciones al navegar entre rutas hijas. A diferencia de los layouts, las plantillas se volverán a renderizar en la navegación. Usar plantillas es excelente para animar elementos durante la navegación.

Ejemplo:

// app/template.js
'use client'

import { useState } from 'react'

export default function Template({ children }) {
  const [count, setCount] = useState(0)

  return (
    <main>
      <p>Plantilla: {count}</p>
      <button onClick={() => setCount(count + 1)}>Actualizar Plantilla</button>
      {children}
    </main>
  )
}

5. El Archivo `loading.js`

El archivo `loading.js` (o `loading.jsx`, `loading.ts`, `loading.tsx`) te permite crear una UI de carga que se muestra mientras un segmento de ruta se está cargando. Esto es útil para proporcionar una mejor experiencia de usuario al obtener datos o realizar otras operaciones asíncronas.

Ejemplo:

// app/about/loading.js
import React from 'react';

export default function Loading() {
  return <p>Cargando información de "sobre nosotros"...</p>;
}

Cuando un usuario navega a `/about`, el componente `Loading` se mostrará hasta que el componente `page.js` esté completamente renderizado.

6. El Archivo `error.js`

El archivo `error.js` (o `error.jsx`, `error.ts`, `error.tsx`) te permite crear una UI de error personalizada que se muestra cuando ocurre un error dentro de un segmento de ruta. Esto es útil para proporcionar un mensaje de error más amigable para el usuario y evitar que toda la aplicación se bloquee.

Ejemplo:

// app/about/error.js
'use client'

import React from 'react';

export default function Error({ error, reset }) {
  return (
    <div>
      <h2>¡Ha ocurrido un error!</h2>
      <p>{error.message}</p>
      <button onClick={() => reset()}>Intentar de nuevo</button>
    </div>
  );
}

Si ocurre un error mientras se renderiza la página `/about`, se mostrará el componente `Error`. La prop `error` contiene información sobre el error, y la función `reset` permite al usuario intentar recargar la página.

7. Grupos de Rutas

Los Grupos de Rutas `(nombreGrupo)` te permiten organizar tus rutas sin afectar la estructura de la URL. Se crean envolviendo el nombre de una carpeta entre paréntesis. Esto es particularmente útil para organizar layouts y componentes compartidos.

Ejemplo:

app/
  (marketing)/
    about/
      page.js
    contact/
      page.js
  (shop)/
    products/
      page.js

En este ejemplo, las páginas `about` y `contact` están agrupadas bajo el grupo `marketing`, y la página `products` está bajo el grupo `shop`. Las URLs permanecen como `/about`, `/contact` y `/products`, respectivamente.

8. Rutas Dinámicas

Las rutas dinámicas te permiten crear rutas con segmentos variables. Esto es útil para mostrar contenido basado en datos obtenidos de una base de datos o API. Los segmentos de ruta dinámicos se definen envolviendo el nombre del segmento en corchetes (p. ej., `[id]`).

Ejemplo:

Digamos que quieres crear una ruta para mostrar publicaciones de blog individuales según su ID. Puedes crear una estructura de archivos como esta:

app/
  blog/
    [id]/
      page.js

El segmento `[id]` es un segmento dinámico. El componente exportado desde `app/blog/[id]/page.js` se renderizará cuando un usuario navegue a una URL como `/blog/123` o `/blog/456`. El valor del parámetro `id` estará disponible en la prop `params` del componente.

// app/blog/[id]/page.js
import React from 'react';

export default async function BlogPost({ params }) {
  const { id } = params;

  // Obtener datos para la publicación del blog con el ID dado
  const post = await fetchBlogPost(id);

  if (!post) {
    return <p>Publicación de blog no encontrada.</p>;
  }

  return (
    <div>
      <h1>{post.title}</h1>
      <p>{post.content}</p>
    </div>
  );
}

async function fetchBlogPost(id) {
  // Simular la obtención de datos de una base de datos o API
  return new Promise((resolve) => {
    setTimeout(() => {
      const posts = {
        '123': { title: 'Mi Primera Publicación de Blog', content: 'Este es el contenido de mi primera publicación de blog.' },
        '456': { title: 'Otra Publicación de Blog', content: 'Este es más contenido emocionante.' },
      };
      resolve(posts[id] || null);
    }, 500);
  });
}

También puedes usar múltiples segmentos dinámicos en una ruta. Por ejemplo, podrías tener una ruta como `/blog/[categoria]/[id]`.

9. Segmentos Catch-all

Los segmentos catch-all (de captura total) te permiten crear rutas que coinciden con cualquier número de segmentos. Esto es útil para escenarios como la creación de un CMS donde la estructura de la URL es determinada por el usuario. Los segmentos catch-all se definen añadiendo tres puntos antes del nombre del segmento (p. ej., `[...slug]`).

Ejemplo:

app/
  docs/
    [...slug]/
      page.js

El segmento `[...slug]` coincidirá con cualquier número de segmentos después de `/docs`. Por ejemplo, coincidirá con `/docs/getting-started`, `/docs/api/users` y `/docs/advanced/configuration`. El valor del parámetro `slug` será un array que contiene los segmentos coincidentes.

// app/docs/[...slug]/page.js
import React from 'react';

export default function DocsPage({ params }) {
  const { slug } = params;

  return (
    <div>
      <h1>Documentación</h1>
      <p>Slug: {slug ? slug.join('/') : 'Sin slug'}</p>
    </div>
  );
}

Los segmentos catch-all opcionales se pueden crear añadiendo el nombre del segmento en dobles corchetes `[[...slug]]`. Esto hace que el segmento de la ruta sea opcional. Ejemplo:

app/
  blog/
    [[...slug]]/
      page.js

Esta configuración renderizará el componente page.js tanto en `/blog` como en `/blog/cualquier/numero/de/segmentos`.

10. Rutas Paralelas

Las Rutas Paralelas te permiten renderizar simultáneamente una o más páginas en el mismo layout. Esto es particularmente útil para layouts complejos, como paneles de control, donde diferentes secciones de la página pueden cargarse de forma independiente. Las Rutas Paralelas se definen usando el símbolo `@` seguido de un nombre de slot (p. ej., `@sidebar`, `@main`).

Ejemplo:

app/
  @sidebar/
    page.js  // Contenido para la barra lateral
  @main/
    page.js  // Contenido para la sección principal
  default.js // Requerido: Define el layout por defecto para las rutas paralelas

El archivo `default.js` es requerido cuando se usan rutas paralelas. Define cómo se combinan los diferentes slots para crear el layout final.

// app/default.js
export default function RootLayout({ children: { sidebar, main } }) {
  return (
    <div style={{ display: 'flex' }}>
      <aside style={{ width: '200px', backgroundColor: '#f0f0f0' }}>
        {sidebar}
      </aside>
      <main style={{ flex: 1, padding: '20px' }}>
        {main}
      </main>
    </div>
  );
}

11. Rutas de Interceptación

Las Rutas de Interceptación te permiten cargar una ruta de una parte diferente de tu aplicación dentro del layout actual. Esto es útil para crear modales, galerías de imágenes y otros elementos de UI que deben aparecer sobre el contenido de la página existente. Las Rutas de Interceptación se definen usando la sintaxis `(..)`, que indica cuántos niveles hacia arriba en el árbol de directorios se debe ir para encontrar la ruta interceptada.

Ejemplo:

app/
  (.)photos/
    [id]/
      page.js  // La ruta interceptada
  feed/
    page.js  // La página donde se muestra el modal de la foto

En este ejemplo, cuando un usuario hace clic en una foto en la página `/feed`, la ruta `app/(.)photos/[id]/page.js` es interceptada y mostrada como un modal sobre la página `/feed`. La sintaxis `(.)` le dice a Next.js que busque un nivel hacia arriba (al directorio `app`) para encontrar la ruta `photos/[id]`.

Obtención de Datos con el App Router

El App Router proporciona soporte integrado para la obtención de datos usando Componentes de Servidor y Componentes de Cliente. Los Componentes de Servidor se renderizan en el servidor, mientras que los Componentes de Cliente se renderizan en el cliente. Esto te permite elegir el mejor enfoque para cada componente según sus requisitos.

Componentes de Servidor

Los Componentes de Servidor son el valor por defecto en el App Router. Te permiten obtener datos directamente en tus componentes sin la necesidad de rutas de API separadas. Esto puede mejorar el rendimiento y simplificar tu código.

Ejemplo:

// app/products/page.js
import React from 'react';

export default async function ProductsPage() {
  const products = await fetchProducts();

  return (
    <div>
      <h1>Productos</h1>
      <ul>
        {products.map((product) => (
          <li key={product.id}>{product.name}</li>
        ))}
      </ul>
    </div>
  );
}

async function fetchProducts() {
  // Simular la obtención de datos de una base de datos o API
  return new Promise((resolve) => {
    setTimeout(() => {
      const products = [
        { id: 1, name: 'Producto A' },
        { id: 2, name: 'Producto B' },
        { id: 3, name: 'Producto C' },
      ];
      resolve(products);
    }, 500);
  });
}

En este ejemplo, la función `fetchProducts` se llama directamente dentro del componente `ProductsPage`. El componente se renderiza en el servidor, y los datos se obtienen antes de que el HTML se envíe al cliente.

Componentes de Cliente

Los Componentes de Cliente se renderizan en el cliente y te permiten usar características del lado del cliente como escuchas de eventos, estado y APIs del navegador. Para usar un Componente de Cliente, necesitas añadir la directiva `'use client'` en la parte superior del archivo.

Ejemplo:

// app/counter/page.js
'use client'

import React, { useState } from 'react';

export default function CounterPage() {
  const [count, setCount] = useState(0);

  return (
    <div>
      <h1>Contador</h1>
      <p>Conteo: {count}</p>
      <button onClick={() => setCount(count + 1)}>Incrementar</button>
    </div>
  );
}

En este ejemplo, el componente `CounterPage` es un Componente de Cliente porque usa el hook `useState`. La directiva `'use client'` le dice a Next.js que renderice este componente en el cliente.

Técnicas de Enrutamiento Avanzadas

El App Router ofrece varias técnicas de enrutamiento avanzadas que se pueden usar para crear aplicaciones complejas y sofisticadas.

1. Manejadores de Ruta (Route Handlers)

Los Manejadores de Ruta te permiten crear puntos finales de API (API endpoints) dentro de tu directorio `app`. Esto elimina la necesidad de un directorio `pages/api` separado. Los Manejadores de Ruta se definen en archivos llamados `route.js` (o `route.ts`) y exportan funciones que manejan diferentes métodos HTTP (p. ej., `GET`, `POST`, `PUT`, `DELETE`).

Ejemplo:

// app/api/users/route.js
import { NextResponse } from 'next/server'

export async function GET(request) {
  // Simular la obtención de usuarios de una base de datos
  const users = [
    { id: 1, name: 'John Doe' },
    { id: 2, name: 'Jane Doe' },
  ];

  return NextResponse.json(users);
}

export async function POST(request) {
  const body = await request.json()
  console.log('Datos recibidos:', body)
  return NextResponse.json({ message: 'Usuario creado' }, { status: 201 })
}

Este ejemplo define un manejador de ruta en `/api/users` que maneja tanto las solicitudes `GET` como `POST`. La función `GET` devuelve una lista de usuarios, y la función `POST` crea un nuevo usuario.

2. Grupos de Rutas con Múltiples Layouts

Puedes combinar grupos de rutas con layouts para crear diferentes diseños para diferentes secciones de tu aplicación. Esto es útil para escenarios en los que deseas tener un encabezado o una barra lateral diferente para distintas partes de tu sitio.

Ejemplo:

app/
  (marketing)/
    layout.js  // Layout de Marketing
    about/
      page.js
    contact/
      page.js
  (admin)/
    layout.js  // Layout de Admin
    dashboard/
      page.js

En este ejemplo, las páginas `about` y `contact` usarán el layout de `marketing`, mientras que la página `dashboard` usará el layout de `admin`.

3. Middleware

El Middleware te permite ejecutar código antes de que una solicitud sea manejada por tu aplicación. Esto es útil para tareas como autenticación, autorización, registro y redirección de usuarios según su ubicación o dispositivo.

El Middleware se define en un archivo llamado `middleware.js` (o `middleware.ts`) en la raíz de tu proyecto.

Ejemplo:

// middleware.js
import { NextResponse } from 'next/server'

export function middleware(request) {
  // Comprobar si el usuario está autenticado
  const isAuthenticated = false; // Reemplaza con tu lógica de autenticación

  if (!isAuthenticated && request.nextUrl.pathname.startsWith('/admin')) {
    return NextResponse.redirect(new URL('/login', request.url));
  }

  return NextResponse.next();
}

// Consulta "Matching Paths" a continuación para saber más
export const config = {
  matcher: '/admin/:path*',
}

Este ejemplo define un middleware que comprueba si el usuario está autenticado antes de permitirle acceder a cualquier ruta bajo `/admin`. Si el usuario no está autenticado, es redirigido a la página `/login`.

Mejores Prácticas para el Enrutamiento Basado en Archivos

Para aprovechar al máximo el sistema de enrutamiento basado en archivos del App Router, considera las siguientes mejores prácticas:

Ejemplos de Internacionalización con el App Router de Next.js

El App Router de Next.js simplifica la internacionalización (i18n) a través del enrutamiento basado en archivos. A continuación se explica cómo puedes implementar i18n de manera efectiva:

1. Enrutamiento por Sub-ruta

Organiza tus rutas basadas en el idioma (locale) usando sub-rutas. Por ejemplo:

app/
  [locale]/
    page.tsx         // Página de inicio para el idioma
    about/
      page.tsx     // Página "acerca de" para el idioma
// app/[locale]/page.tsx
import { getTranslations } from './dictionaries';

export default async function HomePage({ params: { locale } }) {
  const t = await getTranslations(locale);
  return (<h1>{t.home.title}</h1>);
}

// dictionaries.js
const dictionaries = {
  en: () => import('./dictionaries/en.json').then((module) => module.default),
  es: () => import('./dictionaries/es.json').then((module) => module.default),
};

export const getTranslations = async (locale) => {
  try {
    return dictionaries[locale]() ?? dictionaries.en();
  } catch (error) {
    console.error(`No se pudieron cargar las traducciones para el idioma ${locale}`, error);
    return dictionaries.en();
  }
};

En esta configuración, el segmento de ruta dinámico `[locale]` maneja diferentes idiomas (p. ej., `/en`, `/es`). Las traducciones se cargan dinámicamente según el idioma.

2. Enrutamiento por Dominio

Para un enfoque más avanzado, puedes usar diferentes dominios o subdominios para cada idioma. Esto a menudo implica una configuración adicional con tu proveedor de hosting.

3. Middleware para la Detección de Idioma

Usa middleware para detectar automáticamente el idioma preferido del usuario y redirigirlo en consecuencia.

// middleware.js
import { NextResponse } from 'next/server';
import { match } from '@formatjs/intl-localematcher';
import Negotiator from 'negotiator';

let locales = ['en', 'es', 'fr'];

function getLocale(request) {
  const negotiatorHeaders = {};
  request.headers.forEach((value, key) => (negotiatorHeaders[key] = value));
  let languages = new Negotiator({ headers: negotiatorHeaders }).languages();

  try {
      return match(languages, locales, 'en'); // Usar "en" como idioma por defecto
  } catch (error) {
      console.error("Error al buscar el idioma:", error);
      return 'en'; // Volver a inglés si la búsqueda falla
  }
}

export function middleware(request) {
  const pathname = request.nextUrl.pathname;
  const pathnameIsMissingLocale = locales.every(
    (locale) => !pathname.startsWith(`/${locale}/`) && pathname !== `/${locale}`
  );

  if (pathnameIsMissingLocale) {
    const locale = getLocale(request);

    return NextResponse.redirect(
      new URL(
        `/${locale}${pathname.startsWith('/') ? '' : '/'}${pathname}`,
        request.url
      )
    );
  }
}

export const config = {
  matcher: [
    '/((?!api|_next/static|_next/image|favicon.ico).*)',
  ],
};

Este middleware comprueba si la ruta solicitada tiene un prefijo de idioma. Si no es así, detecta el idioma preferido del usuario utilizando la cabecera `Accept-Language` y lo redirige a la ruta específica del idioma apropiado. Bibliotecas como `@formatjs/intl-localematcher` y `negotiator` se utilizan para manejar la negociación del idioma.

App Router de Next.js y Accesibilidad Global

Crear aplicaciones web accesibles a nivel mundial requiere una cuidadosa consideración de los principios de accesibilidad (a11y). El App Router de Next.js proporciona una base sólida para construir experiencias accesibles, pero es esencial implementar las mejores prácticas para asegurar que tu aplicación sea usable por todos, independientemente de sus habilidades.

Consideraciones Clave de Accesibilidad

  1. HTML Semántico: Usa elementos HTML semánticos (p. ej., `<article>`, `<nav>`, `<aside>`, `<main>`) para estructurar tu contenido. Esto proporciona significado a las tecnologías de asistencia y ayuda a los usuarios a navegar por tu sitio más fácilmente.
  2. Atributos ARIA: Usa atributos ARIA (Accessible Rich Internet Applications) para mejorar la accesibilidad de componentes y widgets personalizados. Los atributos ARIA proporcionan información adicional sobre el rol, estado y propiedades de los elementos a las tecnologías de asistencia.
  3. Navegación por Teclado: Asegúrate de que todos los elementos interactivos sean accesibles a través del teclado. Los usuarios deben poder navegar por tu aplicación usando la tecla `Tab` e interactuar con los elementos usando la tecla `Enter` o `Espacio`.
  4. Contraste de Color: Usa un contraste de color suficiente entre el texto y el fondo para asegurar la legibilidad para los usuarios con discapacidades visuales. Las Pautas de Accesibilidad al Contenido Web (WCAG) recomiendan una relación de contraste de al menos 4.5:1 para texto normal y 3:1 para texto grande.
  5. Texto Alternativo de Imagen: Proporciona texto alternativo descriptivo para todas las imágenes. El texto alternativo proporciona una alternativa textual para las imágenes que puede ser leída por los lectores de pantalla.
  6. Etiquetas de Formulario: Asocia las etiquetas de los formularios con sus campos de entrada correspondientes usando el elemento `<label>`. Esto deja claro a los usuarios qué información se espera en cada campo.
  7. Pruebas con Lector de Pantalla: Prueba tu aplicación con un lector de pantalla para asegurar que sea accesible para los usuarios con discapacidades visuales. Los lectores de pantalla populares incluyen NVDA, JAWS y VoiceOver.

Implementando la Accesibilidad en el App Router de Next.js

  1. Usa el Componente Link de Next.js: Usa el componente `<Link>` para la navegación. Proporciona características de accesibilidad incorporadas, como la precarga (prefetching) y la gestión del foco.
  2. Gestión del Foco: Al navegar entre páginas o abrir modales, asegúrate de que el foco se gestione correctamente. El foco debe establecerse en el elemento más lógico de la nueva página o modal.
  3. Componentes Personalizados Accesibles: Al crear componentes personalizados, asegúrate de que sean accesibles siguiendo los principios descritos anteriormente. Usa HTML semántico, atributos ARIA y navegación por teclado para que tus componentes sean usables por todos.
  4. Linting y Pruebas: Usa herramientas de linting como ESLint con plugins de accesibilidad para identificar posibles problemas de accesibilidad en tu código. Además, usa herramientas de prueba automatizadas para probar las violaciones de accesibilidad de tu aplicación.

Conclusión

El sistema de enrutamiento basado en archivos del App Router de Next.js ofrece una forma potente e intuitiva de estructurar y navegar por tus aplicaciones. Al comprender los conceptos básicos y las mejores prácticas descritas en esta guía, puedes construir aplicaciones de Next.js robustas, escalables y mantenibles. Experimenta con las diferentes características del App Router y descubre cómo puede simplificar tu flujo de trabajo de desarrollo y mejorar la experiencia del usuario.