Rutas Dinámicas
Usa la sintaxis de corchetes para crear rutas que coincidan con segmentos de URL dinámicos — parámetros individuales, segmentos catch-all y patrones catch-all opcionales.
Receta
Tarjeta de referencia rápida — lista para copiar y pegar.
app/
├── blog/[slug]/page.tsx # /blog/hello-world → { slug: "hello-world" }
├── docs/[...path]/page.tsx # /docs/a/b/c → { path: ["a", "b", "c"] }
└── shop/[[...categories]]/page.tsx # /shop o /shop/a/b → { categories: ["a", "b"] } o {}
// app/blog/[slug]/page.tsx
export default async function BlogPost({
params,
}: {
params: Promise<{ slug: string }>;
}) {
const { slug } = await params;
return <h1>Post: {slug}</h1>;
}Cuándo usarlo: Cualquier ruta donde la URL contiene una variable — IDs de productos, nombres de usuario, rutas de documentación, o prefijos de idioma.
Ejemplo de Trabajo
// app/blog/[slug]/page.tsx — Segmento dinámico individual
import { notFound } from "next/navigation";
interface Post {
slug: string;
title: string;
content: string;
}
async function getPost(slug: string): Promise<Post | null> {
const res = await fetch(`https://api.example.com/posts/${slug}`, {
next: { revalidate: 3600 },
});
if (!res.ok) return null;
return res.json();
}
export default async function BlogPostPage({
params,
}: {
params: Promise<{ slug: string }>;
}) {
const { slug } = await params;
const post = await getPost(slug);
if (!post) notFound();
return (
<article>
<h1>{post.title}</h1>
<div dangerouslySetInnerHTML={{ __html: post.content }} />
</article>
);
}
// Generación estática para slugs conocidos
export async function generateStaticParams() {
const posts: Post[] = await fetch("https://api.example.com/posts").then((r) =>
r.json()
);
return posts.map((post) => ({ slug: post.slug }));
}// app/docs/[...path]/page.tsx — Ruta catch-all
export default async function DocsPage({
params,
}: {
params: Promise<{ path: string[] }>;
}) {
const { path } = await params;
// /docs/getting-started/install → path = ["getting-started", "install"]
const fullPath = path.join("/");
return (
<div>
<h1>Docs: {fullPath}</h1>
<p>Segmentos: {path.length}</p>
</div>
);
}// app/shop/[[...categories]]/page.tsx — Catch-all opcional
export default async function ShopPage({
params,
}: {
params: Promise<{ categories?: string[] }>;
}) {
const { categories } = await params;
if (!categories || categories.length === 0) {
return <h1>Todos los Productos</h1>;
}
return (
<div>
<h1>Shop: {categories.join(" > ")}</h1>
<p>Filtrando por {categories.length} categorías</p>
</div>
);
}Análisis Profundo
Cómo Funciona
[slug]coincide con un segmento único./blog/hellocoincide,/blog/hello/commentsno.[...path]coincide con uno o más segmentos./docs/acoincide,/docs/a/b/ccoincide, pero/docssolo NO coincide.[[...path]]coincide con cero o más segmentos. Igual que catch-all, pero también coincide con la ruta base (/shopsin segmentos).- Los parámetros se entregan como una
Promiseen Next.js 15+. Debesawait paramsen Server Components ouse(params)en Client Components. generateStaticParamspre-renderiza rutas dinámicas en tiempo de compilación. Devuelve un array de objetos de parámetros, y Next.js genera una página estática para cada uno.- Los segmentos dinámicos pueden estar anidados.
app/[lang]/blog/[slug]/page.tsxproduceparams: { lang, slug }. - Los caminos dinámicos desconocidos caen a través de
not-found.tsxcuando ninguna página coincide, o cuando llamasnotFound().
Variaciones
// Múltiples segmentos dinámicos
// app/[locale]/blog/[slug]/page.tsx
export default async function LocalizedPost({
params,
}: {
params: Promise<{ locale: string; slug: string }>;
}) {
const { locale, slug } = await params;
return <h1>{locale}: {slug}</h1>;
}
export async function generateStaticParams() {
return [
{ locale: "en", slug: "hello" },
{ locale: "fr", slug: "bonjour" },
];
}// generateStaticParams con parámetros del padre
// app/[category]/[product]/page.tsx
export async function generateStaticParams({
params,
}: {
params: { category: string };
}) {
const products = await getProductsByCategory(params.category);
return products.map((p) => ({ product: p.slug }));
}// Ruta dinámica con generateMetadata
// app/blog/[slug]/page.tsx
import type { Metadata } from "next";
export async function generateMetadata({
params,
}: {
params: Promise<{ slug: string }>;
}): Promise<Metadata> {
const { slug } = await params;
const post = await getPost(slug);
return {
title: post?.title ?? "Not Found",
description: post?.content?.slice(0, 160),
};
}Notas de TypeScript
// Tipos de parámetros por patrón
type SingleParam = { slug: string }; // [slug]
type CatchAllParam = { path: string[] }; // [...path]
type OptionalCatchAll = { categories?: string[] }; // [[...categories]]
type MultiParam = { locale: string; slug: string }; // [locale]/[slug]
// Todos envueltos en Promise para las páginas de Next.js 15+
type PageProps = {
params: Promise<SingleParam>;
searchParams: Promise<{ [key: string]: string | string[] | undefined }>;
};
// Tipo de retorno de generateStaticParams
// Devuelve objetos de parámetros SIN el envoltorio Promise
type StaticParams = SingleParam[];Gotchas
- Catch-all
[...path]NO coincide con la ruta base./docsdevolverá 404 a menos que tengasapp/docs/page.tsxpor separado. Usa[[...path]]si necesitas la base. - Los valores de parámetros siempre son strings.
/product/42da{ id: "42" }, no un número. Analiza manualmente. generateStaticParamsse ejecuta en tiempo de compilación. Si tu fuente de datos no está disponible durante la compilación, la compilación falla. UsadynamicParams = true(el valor predeterminado) para permitir renderizado bajo demanda.- El orden importa para rutas superpuestas. Las rutas estáticas tienen prioridad sobre las dinámicas.
/blog/aboutcoincide conapp/blog/about/page.tsxantes deapp/blog/[slug]/page.tsx. - Los parámetros ahora son asíncronos. Desestructurar
{ params: { slug } }directamente en la firma de la función ya no funciona en Next.js 15+. Siempreawait. dynamicParams = falsedevuelve 404 para parámetros no listados. Solo funcionarán los caminos devueltos porgenerateStaticParams.
// Optar por no renderizado dinámico bajo demanda
export const dynamicParams = false;Alternativas
| Enfoque | Cuándo Usarlo |
|---|---|
Ruta estática (app/about/page.tsx) | La URL es fija y conocida de antemano |
Grupos de ruta (group) | Organizar sin agregar segmentos de URL |
| Reescrituras de middleware | Mapear URLs personalizadas a rutas dinámicas existentes |
searchParams en lugar de params de ruta | Filtrado u ordenamiento que no necesita URLs únicas |
FAQs
¿Cuál es la diferencia entre [slug], [...path] y [[...path]]?
[slug]coincide exactamente con un segmento de URL (p. ej.,/blog/hello)[...path]coincide con uno o más segmentos (p. ej.,/docs/a/b/c) pero no con la ruta base[[...path]]coincide con cero o más segmentos, incluida la ruta base (p. ej.,/shopo/shop/a/b)
Gotcha: ¿Por qué /docs devuelve 404 cuando se usa [...path]?
Catch-all [...path] requiere al menos un segmento. /docs solo no coincide. Crea un app/docs/page.tsx separado para la ruta base o cambia a catch-all opcional [[...path]].
¿Cómo accedes a los parámetros en un Server Component en Next.js 15+?
export default async function Page({
params,
}: {
params: Promise<{ slug: string }>;
}) {
const { slug } = await params;
return <h1>{slug}</h1>;
}Debes await params porque ahora son una Promise.
¿Qué hace generateStaticParams y cuándo se ejecuta?
- Se ejecuta en tiempo de compilación para pre-renderizar rutas dinámicas como páginas estáticas
- Devuelve un array de objetos de parámetros, uno por página a generar
- Los caminos desconocidos aún se renderizan bajo demanda a menos que
dynamicParams = false
¿Los valores de parámetros siempre son strings?
Sí. Incluso si la URL contiene un número como /product/42, el valor del parámetro es el string "42". Debes analizarlo manualmente (p. ej., Number(id) o parseInt(id)).
¿Cómo interactúan las rutas estáticas y dinámicas cuando los caminos se superponen?
Las rutas estáticas tienen prioridad. /blog/about coincide con app/blog/about/page.tsx antes de app/blog/[slug]/page.tsx.
¿Qué sucede si estableces dynamicParams = false?
Solo funcionarán los caminos devueltos por generateStaticParams. Cualquier otro camino dinámico devuelve un 404.
export const dynamicParams = false;¿Cómo generas metadatos para una ruta dinámica?
import type { Metadata } from "next";
export async function generateMetadata({
params,
}: {
params: Promise<{ slug: string }>;
}): Promise<Metadata> {
const { slug } = await params;
const post = await getPost(slug);
return { title: post?.title ?? "Not Found" };
}¿Cuáles son los tipos de TypeScript correctos para cada patrón de ruta dinámica?
type SingleParam = { slug: string };
type CatchAllParam = { path: string[] };
type OptionalCatchAll = { categories?: string[] };
type MultiParam = { locale: string; slug: string };
// Todos envueltos en Promise para props de página
type PageProps = {
params: Promise<SingleParam>;
};Gotcha: ¿Puedes desestructurar parámetros directamente en la firma de la función?
No en Next.js 15+. El patrón de desestructuración síncrono { params: { slug } } ya no funciona porque params ahora es una Promise. Siempre await params dentro del cuerpo de la función.
¿Cómo funciona generateStaticParams con segmentos dinámicos anidados?
// app/[category]/[product]/page.tsx
export async function generateStaticParams({
params,
}: {
params: { category: string };
}) {
const products = await getProductsByCategory(params.category);
return products.map((p) => ({ product: p.slug }));
}Los parámetros del segmento padre se pasan al generateStaticParams del hijo.
¿Cómo manejas un recurso faltante en una ruta dinámica?
Llama notFound() desde next/navigation para disparar el not-found.tsx más cercano.
import { notFound } from "next/navigation";
const post = await getPost(slug);
if (!post) notFound();Relacionado
- App Router Basics — descripción general de convenciones de archivos
- Route Groups — organizar sin segmentos de URL
- Navigation — navegación a rutas dinámicas de forma programática
- Loading & Error — estados de carga para páginas dinámicas