Routing en Next.js

Cómo funciona el sistema de routing basado en file system de Next.js App Router: convenciones de archivos, rutas dinámicas, Route Groups, navegación y loading states.

Routing en Next.js

Serie: Entendiendo Next.js — Este artículo es parte de la serie "Entendiendo Next.js", donde exploramos cómo funciona Next.js internamente.

¿Qué es el sistema de routing de Next.js?

En Next.js con App Router, el routing está basado en el file system. Eso significa que la estructura de carpetas dentro de app/ define directamente las URLs de tu aplicación. No necesitás configurar rutas manualmente ni instalar ninguna librería de routing.

Si venís de React puro, estás acostumbrado a configurar React Router con un archivo de rutas o un provider central. En Next.js, eso no existe: la URL /blog/primer-post existe porque hay una carpeta app/blog/primer-post/ con un archivo page.tsx adentro.

Convenciones de archivos

Dentro de cada carpeta de ruta, Next.js reconoce archivos con nombres especiales que cumplen roles específicos:

  • page.tsx — define la UI de esa ruta. Sin este archivo, la ruta no existe aunque la carpeta exista.
  • layout.tsx — UI compartida entre páginas. Se renderiza una sola vez y persiste entre navegaciones sin re-montarse.
  • loading.tsx — UI de fallback que se muestra mientras carga la página. Actúa como un Suspense boundary automático.
  • error.tsx — manejo de errores por segmento de ruta. Aísla los errores para que no rompan toda la app.
  • not-found.tsx — UI que se muestra cuando llamás a notFound() o la ruta no existe.
  • template.tsx — similar a layout.tsx, pero se re-monta en cada navegación.
ArchivoPropósitoSe aplica a
page.tsxUI de la ruta/ruta
layout.tsxUI persistente entre páginas/ruta y sus hijas
loading.tsxFallback de carga/ruta y sus hijas
error.tsxManejo de errores/ruta y sus hijas
not-found.tsxPágina 404/ruta y sus hijas

Rutas anidadas

Anidar carpetas equivale a anidar segmentos en la URL. Cada carpeta representa un segmento de la ruta.

app/
├── page.tsx          → /
├── blog/
│   ├── page.tsx      → /blog
│   └── primer-post/
│       └── page.tsx  → /blog/primer-post
└── dashboard/
    └── page.tsx      → /dashboard

Los layouts también se apilan. Si tenés un layout.tsx en app/ y otro en app/blog/, el layout del blog se renderiza dentro del layout raíz. Ambos persisten mientras navegás dentro de esa sección.

app/layout.tsx           ← siempre presente
└── app/blog/layout.tsx  ← presente solo en rutas /blog/*
    └── app/blog/page.tsx

Rutas dinámicas

Cuando el segmento de la URL varía, usás corchetes para definirlo como dinámico.

  • [slug] — segmento dinámico simple. Captura un único valor.
  • [...slug] — catch-all. Captura uno o más segmentos como array.
  • [[...slug]] — optional catch-all. Captura cero o más segmentos; la ruta base también coincide.
app/
└── blog/
    └── [slug]/
        └── page.tsx   → /blog/cualquier-cosa

Desde Next.js 15 en adelante, params es una Promise y necesitás hacer await para acceder a los valores:

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

type Props = {
  params: Promise<{ slug: string }>
}

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

  // slug es el valor dinámico de la URL: /blog/mi-post → slug = "mi-post"
  const post = await fetch(`https://api.example.com/posts/${slug}`)
    .then(res => res.json())

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

generateStaticParams

Si querés pre-renderizar las rutas dinámicas en build time en lugar de generarlas en cada request, usás generateStaticParams. Next.js va a llamar a esta función durante el build y generar un HTML estático por cada valor que devuelvas.

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

export async function generateStaticParams() {
  const posts = await fetch('https://api.example.com/posts')
    .then(res => res.json())

  // Devolvés un array con los params de cada ruta a pre-renderizar
  return posts.map((post: { slug: string }) => ({
    slug: post.slug,
  }))
}

Route Groups

Los Route Groups te permiten organizar carpetas sin afectar la URL. Para crear uno, envolvés el nombre de la carpeta en paréntesis: (nombre).

app/
├── (marketing)/
│   ├── layout.tsx      ← layout solo para marketing
│   ├── page.tsx        → /
│   └── about/
│       └── page.tsx    → /about
└── (dashboard)/
    ├── layout.tsx      ← layout solo para dashboard
    └── settings/
        └── page.tsx    → /settings

El (marketing) y el (dashboard) no aparecen en la URL. Son transparentes para el router, pero te permiten aplicar layouts distintos a diferentes secciones sin cambiar las URLs.

También podés tener múltiples layouts raíz si cada route group tiene su propio layout.tsx con el elemento <html> y <body>. Esto es útil cuando una parte de la app tiene una estructura HTML completamente diferente, como una sección de docs vs una landing page.

El componente <Link> es la forma principal de navegar entre rutas. Reemplaza el <a> de HTML y agrega prefetch automático: cuando un <Link> aparece en el viewport, Next.js prefetchea la ruta de destino en segundo plano.

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

export default function Nav() {
  return (
    <nav>
      <Link href="/">Inicio</Link>
      <Link href="/blog">Blog</Link>
      <Link href="/about">Nosotros</Link>
    </nav>
  )
}

useRouter()

Para navegar de forma programática, usás el hook useRouter() dentro de un Client Component. Útil después de una acción del usuario, como un submit de formulario.

tsx
// components/LoginForm.tsx
'use client'

import { useRouter } from 'next/navigation'

export default function LoginForm() {
  const router = useRouter()

  async function handleSubmit(e: React.FormEvent) {
    e.preventDefault()
    // ... lógica de login
    router.push('/dashboard')
  }

  return <form onSubmit={handleSubmit}>{/* ... */}</form>
}

usePathname()

Para leer la ruta actual, por ejemplo para marcar el item activo en un menú de navegación, usás usePathname().

tsx
// components/NavLink.tsx
'use client'

import Link from 'next/link'
import { usePathname } from 'next/navigation'

export default function NavLink({ href, label }: { href: string; label: string }) {
  const pathname = usePathname()
  const isActive = pathname === href

  return (
    <Link
      href={href}
      style={{ fontWeight: isActive ? 'bold' : 'normal' }}
    >
      {label}
    </Link>
  )
}

useSearchParams()

Para leer los query params de la URL, como ?page=2 o ?q=next.js, usás useSearchParams(). También requiere un Client Component.

tsx
// components/SearchResults.tsx
'use client'

import { useSearchParams } from 'next/navigation'

export default function SearchResults() {
  const searchParams = useSearchParams()
  const query = searchParams.get('q')  // /search?q=next.js → query = "next.js"

  return <p>Resultados para: {query}</p>
}

Loading states

Cuando creás un archivo loading.tsx en una carpeta de ruta, Next.js lo envuelve automáticamente en un <Suspense> boundary. Mientras la página está cargando, muestra el contenido de loading.tsx de forma inmediata.

app/
└── blog/
    ├── loading.tsx   ← se muestra mientras page.tsx carga
    └── page.tsx
tsx
// app/blog/loading.tsx

export default function Loading() {
  return (
    <div>
      <p>Cargando posts...</p>
    </div>
  )
}

Esto mejora la experiencia porque el usuario ve feedback inmediato en lugar de una pantalla en blanco. También tiene impacto en las métricas de Core Web Vitals, particularmente en el Time to First Byte (TTFB) y el Interaction to Next Paint (INP), ya que la navegación se siente más rápida incluso cuando los datos tardan en llegar.

Tabla resumen

ConvenciónPropósitoEjemplo de ruta
page.tsxDefine la UI de una rutaapp/blog/page.tsx/blog
layout.tsxUI compartida, persiste entre páginasapp/layout.tsx → toda la app
loading.tsxFallback de carga (Suspense automático)app/blog/loading.tsx
error.tsxManejo de errores por segmentoapp/blog/error.tsx
[slug]Segmento dinámico simpleapp/blog/[slug]/page.tsx
[...slug]Catch-all (uno o más segmentos)app/docs/[...slug]/page.tsx
[[...slug]]Optional catch-all (cero o más)app/[[...slug]]/page.tsx
(nombre)Route Group, no afecta la URLapp/(marketing)/page.tsx/
Emanuel López

Emanuel López

Desarrollador de Software · Montevideo, Uruguay

Mantente al día

Te aviso cuando publique algo nuevo. Podés darte de baja cuando quieras.