Sistema de Enrutamiento en Next.js

Next.js usa un sistema de enrutamiento basado en archivos donde la estructura de carpetas define automáticamente las rutas de tu aplicación.

Cómo se construyen las rutas


En Next.js 13+ con App Router, cada carpeta representa un segmento de ruta y debe contener un archivo page.tsx para ser accesible:

  Estructura básica

  • Carpeta = Segmento de ruta
  • page.tsx = Página accesible públicamente
  • layout.tsx = Layout compartido para rutas hijas
  • loading.tsx = UI de carga
  • error.tsx = UI de error

bash
app/
├── page.tsx                 # → /
├── about/
│   └── page.tsx            # → /about
├── blog/
│   ├── page.tsx            # → /blog
│   └── [slug]/
│       └── page.tsx        # → /blog/cualquier-slug
└── dashboard/
  ├── layout.tsx          # Layout para /dashboard/*
  ├── page.tsx            # → /dashboard
  └── settings/
      └── page.tsx        # → /dashboard/settings

Ruteo básico y sistema por archivos


1Páginas básicas

tsx
// app/page.tsx - Página principal (/)
export default function HomePage() {
return (
  <div>
    <h1>Página Principal</h1>
    <p>Bienvenido a mi sitio web</p>
  </div>
)
}
tsx
// app/about/page.tsx - Página sobre nosotros (/about)
export default function AboutPage() {
return (
  <div>
    <h1>Sobre Nosotros</h1>
    <p>Información de la empresa</p>
  </div>
)
}
tsx
// app/contact/page.tsx - Página de contacto (/contact)
export default function ContactPage() {
return (
  <div>
    <h1>Contacto</h1>
    <form>
      <input type="email" placeholder="Tu email" />
      <textarea placeholder="Tu mensaje"></textarea>
      <button type="submit">Enviar</button>
    </form>
  </div>
)
}

2Navegación entre páginas

tsx
// components/Navbar.tsx
import Link from 'next/link'

export default function Navbar() {
return (
  <nav className="bg-blue-600 text-white p-4">
    <div className="flex space-x-4">
      <Link href="/" className="hover:underline">
        Inicio
      </Link>
      <Link href="/about" className="hover:underline">
        Sobre Nosotros
      </Link>
      <Link href="/contact" className="hover:underline">
        Contacto
      </Link>
      <Link href="/blog" className="hover:underline">
        Blog
      </Link>
    </div>
  </nav>
)
}

Rutas anidadas


Las rutas anidadas se crean con carpetas dentro de carpetas:

bash
app/
└── dashboard/
  ├── layout.tsx          # Layout compartido
  ├── page.tsx            # /dashboard
  ├── analytics/
  │   └── page.tsx        # /dashboard/analytics
  ├── users/
  │   ├── page.tsx        # /dashboard/users
  │   └── [id]/
  │       └── page.tsx    # /dashboard/users/123
  └── settings/
      ├── page.tsx        # /dashboard/settings
      └── profile/
          └── page.tsx    # /dashboard/settings/profile

Layout compartido para rutas anidadas

tsx
// app/dashboard/layout.tsx
export default function DashboardLayout({
children,
}: {
children: React.ReactNode
}) {
return (
  <div className="min-h-screen bg-gray-100">
    {/* Sidebar */}
    <aside className="w-64 bg-white shadow-md fixed h-full">
      <div className="p-4">
        <h2 className="text-xl font-bold">Dashboard</h2>
        <nav className="mt-4 space-y-2">
          <a href="/dashboard" className="block p-2 hover:bg-gray-100">
            Inicio
          </a>
          <a href="/dashboard/analytics" className="block p-2 hover:bg-gray-100">
            Analytics
          </a>
          <a href="/dashboard/users" className="block p-2 hover:bg-gray-100">
            Usuarios
          </a>
          <a href="/dashboard/settings" className="block p-2 hover:bg-gray-100">
            Configuración
          </a>
        </nav>
      </div>
    </aside>
    
    {/* Contenido principal */}
    <main className="ml-64 p-8">
      {children}
    </main>
  </div>
)
}
tsx
// app/dashboard/page.tsx
export default function DashboardPage() {
return (
  <div>
    <h1 className="text-2xl font-bold mb-4">Dashboard Principal</h1>
    <div className="grid grid-cols-3 gap-4">
      <div className="bg-white p-4 rounded shadow">
        <h3>Usuarios</h3>
        <p className="text-2xl font-bold">1,234</p>
      </div>
      <div className="bg-white p-4 rounded shadow">
        <h3>Ventas</h3>
        <p className="text-2xl font-bold">$12,345</p>
      </div>
      <div className="bg-white p-4 rounded shadow">
        <h3>Pedidos</h3>
        <p className="text-2xl font-bold">567</p>
      </div>
    </div>
  </div>
)
}

Rutas dinámicas


Las rutas dinámicas usan corchetes [parametro] para capturar valores variables:

1Ruta dinámica simple

bash
app/
└── blog/
  ├── page.tsx            # /blog
  └── [slug]/
      └── page.tsx        # /blog/mi-primer-post
tsx
// app/blog/[slug]/page.tsx
interface Props {
params: { slug: string }
}

export default function BlogPost({ params }: Props) {
const { slug } = params

return (
  <div>
    <h1>Post: {slug}</h1>
    <p>Contenido del post con slug: {slug}</p>
  </div>
)
}

// Ejemplos de URLs:
// /blog/mi-primer-post → slug = "mi-primer-post"
// /blog/como-usar-nextjs → slug = "como-usar-nextjs"

2Múltiples parámetros dinámicos

bash
app/
└── shop/
  └── [category]/
      └── [product]/
          └── page.tsx    # /shop/electronics/laptop
tsx
// app/shop/[category]/[product]/page.tsx
interface Props {
params: { 
  category: string
  product: string 
}
}

export default function ProductPage({ params }: Props) {
const { category, product } = params

return (
  <div>
    <nav>
      <span>Tienda</span><span>{category}</span><span>{product}</span>
    </nav>
    
    <h1>Producto: {product}</h1>
    <p>Categoría: {category}</p>
  </div>
)
}

// Ejemplos:
// /shop/electronics/laptop → category="electronics", product="laptop"
// /shop/clothing/shirt → category="clothing", product="shirt"

3Catch-all routes

Usa [...parametro] para capturar múltiples segmentos:

bash
app/
└── docs/
  └── [...slug]/
      └── page.tsx        # /docs/a/b/c/d
tsx
// app/docs/[...slug]/page.tsx
interface Props {
params: { slug: string[] }
}

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

return (
  <div>
    <h1>Documentación</h1>
    <p>Ruta: /{slug.join('/')}</p>
    <ul>
      {slug.map((segment, index) => (
        <li key={index}>Segmento {index + 1}: {segment}</li>
      ))}
    </ul>
  </div>
)
}

// Ejemplos:
// /docs/getting-started → slug = ["getting-started"]
// /docs/api/users/create → slug = ["api", "users", "create"]

Rutas ocultas (Private folders)


Las carpetas que empiezan con _ son privadas y no crean rutas:

bash
app/
├── _components/           # No crea ruta (privada)
│   ├── Header.tsx
│   └── Footer.tsx
├── _lib/                  # No crea ruta (privada)
│   └── utils.ts
├── page.tsx               # → /
└── about/
  └── page.tsx           # → /about
  Carpetas privadas

Las carpetas privadas son útiles para organizar componentes, utilidades y archivos que no deben ser rutas públicas.

Rutas agrupadas


Usa paréntesis (grupo) para organizar rutas sin afectar la URL:

bash
app/
├── (marketing)/           # Grupo - no afecta URL
│   ├── layout.tsx         # Layout para marketing
│   ├── about/
│   │   └── page.tsx       # → /about
│   └── contact/
│       └── page.tsx       # → /contact
├── (shop)/                # Grupo - no afecta URL
│   ├── layout.tsx         # Layout para tienda
│   ├── products/
│   │   └── page.tsx       # → /products
│   └── cart/
│       └── page.tsx       # → /cart
└── page.tsx               # → /

Layout específico para cada grupo

tsx
// app/(marketing)/layout.tsx
export default function MarketingLayout({
children,
}: {
children: React.ReactNode
}) {
return (
  <div>
    {/* Header específico para marketing */}
    <header className="bg-blue-600 text-white p-4">
      <h1>Mi Empresa</h1>
      <nav>
        <a href="/about">Sobre Nosotros</a>
        <a href="/contact">Contacto</a>
      </nav>
    </header>
    
    <main>{children}</main>
    
    {/* Footer específico para marketing */}
    <footer className="bg-gray-800 text-white p-4">
      <p>&copy; 2024 Mi Empresa</p>
    </footer>
  </div>
)
}
tsx
// app/(shop)/layout.tsx
export default function ShopLayout({
children,
}: {
children: React.ReactNode
}) {
return (
  <div>
    {/* Header específico para tienda */}
    <header className="bg-green-600 text-white p-4">
      <h1>Mi Tienda</h1>
      <nav>
        <a href="/products">Productos</a>
        <a href="/cart">Carrito (3)</a>
      </nav>
    </header>
    
    <main>{children}</main>
  </div>
)
}

Rutas Paralelas


Las rutas paralelas permiten renderizar múltiples páginas simultáneamente usando slots @nombre:

bash
app/
└── dashboard/
  ├── layout.tsx
  ├── page.tsx
  ├── @analytics/         # Slot paralelo
  │   └── page.tsx
  ├── @notifications/     # Slot paralelo
  │   └── page.tsx
  └── @users/             # Slot paralelo
      └── page.tsx
tsx
// app/dashboard/layout.tsx
export default function DashboardLayout({
children,
analytics,
notifications,
users,
}: {
children: React.ReactNode
analytics: React.ReactNode
notifications: React.ReactNode
users: React.ReactNode
}) {
return (
  <div className="grid grid-cols-2 gap-4 p-4">
    {/* Contenido principal */}
    <div className="col-span-2">
      {children}
    </div>
    
    {/* Slots paralelos */}
    <div className="bg-white p-4 rounded shadow">
      <h2>Analytics</h2>
      {analytics}
    </div>
    
    <div className="bg-white p-4 rounded shadow">
      <h2>Notificaciones</h2>
      {notifications}
    </div>
    
    <div className="col-span-2 bg-white p-4 rounded shadow">
      <h2>Usuarios</h2>
      {users}
    </div>
  </div>
)
}
tsx
// app/dashboard/@analytics/page.tsx
export default function AnalyticsSlot() {
return (
  <div>
    <p>Visitantes hoy: 1,234</p>
    <p>Páginas vistas: 5,678</p>
  </div>
)
}

Error 404


1Página 404 global

tsx
// app/not-found.tsx
import Link from 'next/link'

export default function NotFound() {
return (
  <div className="min-h-screen flex items-center justify-center">
    <div className="text-center">
      <h1 className="text-6xl font-bold text-gray-400">404</h1>
      <h2 className="text-2xl font-semibold mt-4">Página no encontrada</h2>
      <p className="text-gray-600 mt-2">
        La página que buscas no existe o ha sido movida.
      </p>
      
      <div className="mt-6 space-x-4">
        <Link 
          href="/" 
          className="bg-blue-600 text-white px-4 py-2 rounded hover:bg-blue-700"
        >
          Ir al inicio
        </Link>
        <Link 
          href="/contact" 
          className="border border-gray-300 px-4 py-2 rounded hover:bg-gray-50"
        >
          Contactar soporte
        </Link>
      </div>
    </div>
  </div>
)
}

2404 específico por sección

tsx
// app/blog/not-found.tsx
import Link from 'next/link'

export default function BlogNotFound() {
return (
  <div className="max-w-2xl mx-auto text-center py-12">
    <h1 className="text-4xl font-bold text-gray-800">Post no encontrado</h1>
    <p className="text-gray-600 mt-4">
      El artículo que buscas no existe o ha sido eliminado.
    </p>
    
    <div className="mt-8">
      <Link 
        href="/blog" 
        className="bg-blue-600 text-white px-6 py-3 rounded-lg hover:bg-blue-700"
      >
        Ver todos los posts
      </Link>
    </div>
    
    {/* Posts relacionados */}
    <div className="mt-12">
      <h3 className="text-xl font-semibold mb-4">Posts populares</h3>
      <div className="space-y-2">
        <Link href="/blog/como-usar-nextjs" className="block hover:underline">
          Cómo usar Next.js
        </Link>
        <Link href="/blog/react-hooks" className="block hover:underline">
          Guía de React Hooks
        </Link>
      </div>
    </div>
  </div>
)
}

3Redirección programática a 404

tsx
// app/blog/[slug]/page.tsx
import { notFound } from 'next/navigation'

interface Props {
params: { slug: string }
}

// Simulamos una base de datos de posts
const posts = [
{ slug: 'como-usar-nextjs', title: 'Cómo usar Next.js' },
{ slug: 'react-hooks', title: 'Guía de React Hooks' },
]

export default function BlogPost({ params }: Props) {
const { slug } = params

// Buscar el post
const post = posts.find(p => p.slug === slug)

// Si no existe, mostrar 404
if (!post) {
  notFound()
}

return (
  <article>
    <h1>{post.title}</h1>
    <p>Contenido del post...</p>
  </article>
)
}
  Resumen del Sistema de Rutas

Next.js hace el enrutamiento súper simple: carpetas = rutas, page.tsx = página pública. Con rutas dinámicas, agrupadas y paralelas tienes control total sobre la estructura de tu app.

Ejemplo completo: Estructura de una app real

bash
app/
├── layout.tsx                    # Layout raíz
├── page.tsx                      # → /
├── not-found.tsx                 # 404 global
├── (marketing)/                  # Grupo marketing
│   ├── layout.tsx               # Layout marketing
│   ├── about/
│   │   └── page.tsx             # → /about
│   └── contact/
│       └── page.tsx             # → /contact
├── (shop)/                       # Grupo tienda
│   ├── layout.tsx               # Layout tienda
│   ├── products/
│   │   ├── page.tsx             # → /products
│   │   └── [id]/
│   │       └── page.tsx         # → /products/123
│   └── cart/
│       └── page.tsx             # → /cart
├── blog/
│   ├── page.tsx                 # → /blog
│   ├── not-found.tsx            # 404 del blog
│   └── [slug]/
│       └── page.tsx             # → /blog/mi-post
├── dashboard/
│   ├── layout.tsx               # Layout dashboard
│   ├── page.tsx                 # → /dashboard
│   ├── @analytics/              # Slot paralelo
│   │   └── page.tsx
│   └── users/
│       ├── page.tsx             # → /dashboard/users
│       └── [id]/
│           └── page.tsx         # → /dashboard/users/123
└── _components/                  # Carpeta privada
  ├── Header.tsx
  └── Footer.tsx

Con esta estructura tienes una aplicación completa con diferentes secciones, layouts específicos y manejo de errores. ¡El sistema de rutas de Next.js hace todo el trabajo pesado por ti!