Next.js
Dynamic routes y parámetros
Explica segmentos dinámicos, catch-all, params asíncronos, validación, canonicalización, generateStaticParams y autorización por recurso en Next.js.
- Última actualización
- Actualizada
- Nivel
- Fundamentos
Next.js
Explica segmentos dinámicos, catch-all, params asíncronos, validación, canonicalización, generateStaticParams y autorización por recurso en Next.js.
Una ruta dinámica convierte parte de la URL en datos de entrada. El nombre de la carpeta describe la forma esperada, pero no valida el valor, no garantiza que el recurso exista y no autoriza al usuario para acceder a él.
Los segmentos dinámicos permiten que una sola page represente múltiples entidades:
app/products/[slug]/page.tsx
/products/coffee
/products/tea
/products/chocolate[slug] captura exactamente un segmento. Para más de uno existen catch-all y optional catch-all:
[...parts] → uno o más segmentos
[[...parts]] → cero o más segmentosEl router entrega estos valores mediante params.
Sin rutas dinámicas tendrías que crear un archivo por producto o mantener una tabla de rutas manual. Una ruta parametrizada expresa que varias URLs comparten estructura, pero seleccionan datos diferentes:
pathname pattern
+ parameter values
+ data lookup
= page instanceLa page se vuelve un adaptador entre una URL pública y una entidad del dominio.
En Next.js 16, params y searchParams se consumen de forma asíncrona en pages y layouts:
// app/products/[slug]/page.tsx
type ProductPageProps = {
params: Promise<{ slug: string }>;
};
export default async function ProductPage({ params }: ProductPageProps) {
const { slug } = await params;
return <h1>{slug}</h1>;
}Next.js también genera helpers globales como PageProps durante next dev, next build o next typegen:
export default async function ProductPage({
params,
}: PageProps<"/products/[slug]">) {
const { slug } = await params;
return <h1>{slug}</h1>;
}El helper infiere la forma según la ruta. Sigue sin validar el contenido.
app/users/[userId]/page.tsxValores:
/users/42 → { userId: "42" }
/users/abc → { userId: "abc" }
/users/a%20b → { userId: "a b" }Todo llega como string. Un tipo userId: string no prueba que sea UUID, entero ni un ID al que el usuario pueda acceder.
app/docs/[...parts]/page.tsx/docs/react/hooks/use-state
→ { parts: ["react", "hooks", "use-state"] }No coincide con /docs porque exige al menos un segmento.
Uso:
export default async function DocsPage({
params,
}: PageProps<"/docs/[...parts]">) {
const { parts } = await params;
const path = parts.join("/");
return <Document path={path} />;
}app/shop/[[...filters]]/page.tsxCoincidencias:
/shop → filters undefined
/shop/coffee → ["coffee"]
/shop/coffee/organic → ["coffee", "organic"]Debes modelar la ausencia:
const segments = filters ?? [];No uses optional catch-all solo para evitar diseñar URLs claras. Puede aceptar demasiadas combinaciones y complicar canonicalización.
import { z } from "zod";
const productSlugSchema = z
.string()
.min(1)
.max(120)
.regex(/^[a-z0-9]+(?:-[a-z0-9]+)*$/);
export default async function ProductPage({
params,
}: PageProps<"/products/[slug]">) {
const { slug: rawSlug } = await params;
const parsed = productSlugSchema.safeParse(rawSlug);
if (!parsed.success) {
notFound();
}
const product = await getProductBySlug(parsed.data);
if (!product) {
notFound();
}
return <ProductDetails product={product} />;
}La validación evita consultas innecesarias y ataques sobre formatos esperados, pero no implementa autorización.
/products/7e2f...Suele ser estable y adecuado para relaciones internas.
/products/cafe-colombianoEs legible y útil para URLs públicas, pero puede cambiar si cambia el nombre.
/products/7e2f-cafe-colombianoEl ID mantiene identidad y el slug aporta lectura. Si el slug no coincide, puedes redirigir a la URL canónica.
No existe una opción universal. Evalúa estabilidad, SEO, privacidad y longitud.
Entradas equivalentes pueden producir contenido duplicado:
/products/Coffee
/products/coffee/
/products/coffee?ref=xDefine reglas:
La Metadata API debe generar canonical coherente. Un canonical no sustituye redirects cuando quieres una única URL navegable.
Guarda historial o aliases:
const result = await resolveProductSlug(slug);
if (!result) notFound();
if (result.canonicalSlug !== slug) {
permanentRedirect(`/products/${result.canonicalSlug}`);
}Una redirección permanente conserva enlaces antiguos y comunica la URL preferida.
Permite declarar parámetros conocidos durante generación:
export async function generateStaticParams() {
const products = await listPublishedProducts();
return products.map((product) => ({
slug: product.slug,
}));
}La configuración del segmento puede controlar qué ocurre con parámetros no generados. Si se deshabilitan, una ruta fuera del conjunto conocido responde not found.
Úsalo cuando el conjunto debe quedar cerrado. Para catálogos que crecen después del build, permitir rutas bajo demanda suele ser necesario.
Un layout dinámico puede leer parámetros de su nivel y superiores:
export default async function OrganizationLayout({
children,
params,
}: LayoutProps<"/organizations/[organizationId]">) {
const { organizationId } = await params;
const organization = await requireOrganization(organizationId);
return (
<OrganizationContextProvider initialValue={organization}>
{children}
</OrganizationContextProvider>
);
}Solo eleva la consulta si todos los descendientes necesitan ese contexto.
"use client";
import { useParams } from "next/navigation";
export function ProductActions() {
const { slug } = useParams<{ slug: string }>();
return <button>Editar {slug}</button>;
}El generic solo ayuda a TypeScript. La page servidor es una mejor frontera para validación y autorización inicial.
Los search params no forman parte de la identidad del segmento:
/products?query=coffee&page=2En una page:
export default async function ProductsPage({
searchParams,
}: PageProps<"/products">) {
const raw = await searchParams;
const filters = productsFilterSchema.parse(raw);
return <ProductsView filters={filters} />;
}Una query puede contener valores repetidos o ausentes. Modela string | string[] | undefined cuando trabajes con la forma genérica.
app/organizations/[organizationId]/projects/[projectId]/page.tsxexport default async function ProjectPage({
params,
}: PageProps<"/organizations/[organizationId]/projects/[projectId]">) {
const { organizationId, projectId } = await params;
const session = await requireSession();
const project = await getProjectForOrganization({
organizationId,
projectId,
userId: session.user.id,
});
if (!project) notFound();
return <ProjectDetails project={project} />;
}Consultar únicamente por projectId y después comprobar organización puede abrir errores de acceso. La capa de datos debe incorporar tenant y usuario en la consulta autorizada.
URL inputs
+ session identity
+ authorization rule
→ accessible project or absenceNo confíes en:
/admin.generateStaticParams.Cada acceso debe verificar que la identidad actual puede leer o modificar el recurso.
IDs secuenciales como /orders/1, /orders/2 facilitan enumeración. Usar UUID reduce predictibilidad, pero no reemplaza permisos.
Cuando la existencia de una entidad es sensible, puedes responder not found al usuario no autorizado.
Next.js prioriza rutas estáticas sobre dinámicas cuando corresponda:
app/blog/new/page.tsx
app/blog/[slug]/page.tsx/blog/new debe representar la ruta estática.
Evita usar valores reservados como slugs si crean ambigüedad de producto.
Los valores pueden contener caracteres codificados. Construye links con encoding y valida la forma decodificada.
No uses directamente un parámetro para:
El router no sanitiza esos sinks por ti.
La page debe manejar ausencia en el momento de lectura y revalidar la representación.
La base de datos necesita una restricción de unicidad dentro del alcance correcto, por ejemplo tenant + slug.
?tag=a&tag=b puede producir array. Define si se admite.
Limita longitud antes de consultar o registrar.
En optional catch-all, el valor puede ser undefined.
Nunca uses solo el parámetro de organización como prueba de acceso.
Prueba:
generateStaticParams con datos que cambian después del build.El tipo desaparece en runtime. Valida con schema.
Los nombres cambian. Guarda ID y aliases.
Puede filtrar datos entre organizaciones.
Acepta paths difíciles de mantener y puede ocultar 404 reales.
Miles o millones de params aumentan build. Prerenderiza rutas valiosas y deja otras bajo demanda cuando el modelo lo permite.
Introduce inyección. Utiliza queries parametrizadas y repositorios.
[id]: entidad o categoría de un nivel.[...parts]: jerarquía con al menos un segmento, como documentación.[[...parts]]: raíz y jerarquía en la misma page./new o /settings.generateStaticParams informa prerendering, no permisos.PageProps no elimina la necesidad de Zod?[...parts] y [[...parts]]?generateStaticParams no protege una ruta privada?Route groups y private folders explica cómo organizar el árbol sin cambiar URLs y sin convertir cada decisión de carpetas en arquitectura pública.