En aplicaciones web a escala empresarial, la gran mayoría de las direcciones URL no se pueden fijar estáticamente durante el desarrollo. Catálogos de productos, artículos de blogs, documentación técnica y perfiles de usuario gestionan cientos de miles de entidades. Crear archivos físicos para cada URL individual resulta ineficiente e insostenible a nivel de arquitectura.
Next.js App Router resuelve este reto mediante segmentos parametrizados del sistema de archivos: las rutas dinámicas (Dynamic Routes). En lugar de un nombre de carpeta estático, el desarrollador declara una plantilla parametrizada entre corchetes. El compilador Turbopack compila esta jerarquía en un nodo dinámico dentro de un árbol de prefijos (Radix Tree), extrayendo los valores de la URL y entregándolos de forma segura al componente de página, layout o función generateMetadata.
En App Router, un segmento dinámico (Dynamic Segment) se define encerrando el nombre del directorio entre corchetes. Por ejemplo, la carpeta app/products/[id] intercepta peticiones como /products/101 o /products/macbook-pro, asignando el valor del segmento a la clave id.
[param]): Captura estrictamente un único segmento URL entre dos barras (/blog/nextjs).[...param]): Intercepta múltiples segmentos anidados en un array de strings. Una petición a la ruta raíz /docs sin parámetros devolverá un error 404.[[...param]]): Coincide tanto con subrutas anidadas como con la propia ruta base (donde el parámetro se resuelve como undefined).Cuando una URL entrante coincide simultáneamente con varias estructuras de carpetas, Turbopack resuelve el conflicto según una estricta precedencia de rutas (Route Precedence): Rutas estáticas > Segmentos dinámicos [id] > Catch-all [...slug] > Optional Catch-all [[...slug]].
Turbopack divide la URI HTTP entrante en tokens mediante el delimitador de barra: /docs/api/v1 -> ['docs', 'api', 'v1'].
El motor recorre el árbol de prefijos Radix evaluando primero rutas estáticas, luego nodos simples [param] y finalmente patrones greedy [...slug].
Los valores extraídos se empaquetan en un Promise<params> para permitir el streaming independiente del shell estático (PPR).
El servidor inicia la ejecución concurrente de page.tsx y generateMetadata. Las llamadas await params desbloquean las variables de ruta.
El RSC Flight Payload se transmite al navegador. Los componentes de cliente leen los parámetros mediante el hook use(paramsPromise) de React 19.
¿Por qué en Next.js 15+ y React 19 la propiedad params en page.tsx se declara como Promise<{ slug: string }> y no como un objeto síncrono?
import { Metadata } from 'next';
import { notFound } from 'next/navigation';
interface BlogPostPageProps {
params: Promise<{ slug: string }>;
searchParams: Promise<{ ref?: string }>;
}
export async function generateMetadata(props: BlogPostPageProps): Promise<Metadata> {
const { slug } = await props.params;
return {
title: `Artículo: ${slug}`,
description: `Análisis arquitectónico en profundidad de ${slug}`,
};
}
export default async function BlogPostPage(props: BlogPostPageProps) {
const { slug } = await props.params;
const { ref } = await props.searchParams;
if (slug === 'forbidden-topic') {
notFound();
}
return (
<article className="p-6">
<h1 className="text-2xl font-bold">Publicación: {slug}</h1>
{ref && <p className="text-xs text-text-muted">Fuente de referencia: {ref}</p>}
</article>
);
}Cuando se requiere consumir una promesa de parámetros directamente en un componente de cliente:
'use client';
import { use } from 'react';
interface SlugBadgeProps {
paramsPromise: Promise<{ slug: string }>;
}
export function SlugBadge({ paramsPromise }: SlugBadgeProps) {
// El hook use() desenvuelve la Promise directamente en el ciclo de render del cliente
const { slug } = use(paramsPromise);
return (
<span className="px-2 py-1 rounded bg-accent-violet/10 text-accent-violet border border-accent-violet/20 font-mono text-xs">
Slug activo: {slug}
</span>
);
}La función generateStaticParams() permite pre-renderizar páginas HTML estáticas para rutas dinámicas durante el build (next build), logrando rendimiento SSG:
// 1. Declarar la lista de rutas para pre-renderizar en compilación
export async function generateStaticParams() {
const products = [
{ id: 'prod-1' },
{ id: 'prod-2' },
{ id: 'prod-3' },
];
return products.map((product) => ({
id: product.id,
}));
}
// 2. Control de fallback para parámetros no generados previamente
// true: genera la página bajo demanda en la primera petición (Incremental Static Regeneration, ISR)
// false: retorna inmediatamente 404 para cualquier parámetro ausente en generateStaticParams
export const dynamicParams = true;
interface ProductPageProps {
params: Promise<{ id: string }>;
}
export default async function ProductPage({ params }: ProductPageProps) {
const { id } = await params;
return <div>Producto: {id}</div>;
}¿Qué sucede si se llama a la misma función fetch('https://api.example.com/products') tanto en generateStaticParams() como dentro de ProductPage?
interface DocsPageProps {
params: Promise<{ slug: string[] }>;
}
export async function generateStaticParams() {
return [
{ slug: ['getting-started'] },
{ slug: ['architecture', 'routing'] },
{ slug: ['architecture', 'rendering', 'rsc'] },
];
}
export default async function DocsPage({ params }: DocsPageProps) {
const { slug } = await params;
const currentPath = slug.join(' / ');
return (
<div className="space-y-3 p-6">
<div className="text-xs text-text-muted font-mono">
Miga de pan: docs / {currentPath}
</div>
<h1 className="text-2xl font-bold">
Sección: {slug[slug.length - 1]}
</h1>
<p className="text-sm text-text-secondary">
Profundidad de segmento: {slug.length}
</p>
</div>
);
}El segmento app/shop/[[...slug]]/page.tsx maneja tanto la tienda principal /shop como categorías anidadas /shop/shoes/nike:
interface ShopPageProps {
params: Promise<{ slug?: string[] }>;
}
export default async function ShopPage({ params }: ShopPageProps) {
const { slug } = await params;
// Ruta base /shop (slug es undefined)
if (!slug || slug.length === 0) {
return <h1>Escaparate principal (Todas las categorías)</h1>;
}
// Ruta anidada /shop/category/subcategory
return (
<div>
<h2>Filtro de productos: {slug.join(' > ')}</h2>
</div>
);
}Acceder a props.params.slug sin await o el hook use() genera advertencias y errores en builds de producción:
Error: Route "[slug]" used params.slug. params should be awaited before using its properties.
Solución: Desestructura siempre tras await: const { slug } = await props.params;.
Si existen app/posts/new/page.tsx y app/posts/[id]/page.tsx, las peticiones a /posts/new son capturadas por el archivo estático debido a la Precedencia de Rutas. Sin embargo, asumir lógica dinámica para new provocará comportamientos inesperados.
Solución: Define rutas de gestión estáticas (new, edit, settings) en carpetas explícitas y evita que IDs dinámicos coincidan con palabras reservadas.
Definir export const dynamicParams = false; obliga al servidor a devolver 404 para cualquier URL no listada en generateStaticParams(). Si se agrega un nuevo registro en la base de datos tras el build, los usuarios no podrán verlo hasta el próximo despliegue.
Solución: Conserva dynamicParams = true (por defecto) si necesitas regeneración bajo demanda con Incremental Static Regeneration (ISR).