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.

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 anotFound()o la ruta no existe.template.tsx— similar alayout.tsx, pero se re-monta en cada navegación.
| Archivo | Propósito | Se aplica a |
|---|---|---|
page.tsx | UI de la ruta | /ruta |
layout.tsx | UI persistente entre páginas | /ruta y sus hijas |
loading.tsx | Fallback de carga | /ruta y sus hijas |
error.tsx | Manejo de errores | /ruta y sus hijas |
not-found.tsx | Pá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:
// 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.
// 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.
Navegación entre rutas
<Link>
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.
// 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.
// 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().
// 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.
// 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
// 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ón | Propósito | Ejemplo de ruta |
|---|---|---|
page.tsx | Define la UI de una ruta | app/blog/page.tsx → /blog |
layout.tsx | UI compartida, persiste entre páginas | app/layout.tsx → toda la app |
loading.tsx | Fallback de carga (Suspense automático) | app/blog/loading.tsx |
error.tsx | Manejo de errores por segmento | app/blog/error.tsx |
[slug] | Segmento dinámico simple | app/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 URL | app/(marketing)/page.tsx → / |
Seguir leyendo
Mantente al día
Te aviso cuando publique algo nuevo. Podés darte de baja cuando quieras.


