40 Reglas de Next.js
Reglas enfocadas específicamente en construir aplicaciones Next.js con el App Router. Arquitectura, enrutamiento, renderizado, despliegue y prácticas operacionales.
Enrutamiento y Convenciones de Archivos (Reglas 1-10)
1. Una ruta, una responsabilidad. Cada page.tsx debe representar una única vista. Si un archivo de página crece más allá de 100 líneas, extrae componentes a archivos colocalizados.
2. Usa grupos de rutas para organizar sin afectar las URLs. (marketing), (dashboard), (auth) mantienen tu árbol de archivos limpio sin agregar segmentos de URL.
app/
(marketing)/
page.tsx # /
pricing/page.tsx # /pricing
(dashboard)/
dashboard/page.tsx # /dashboard
settings/page.tsx # /settings
3. Siempre proporciona loading.tsx para rutas dinámicas. Cada segmento de ruta que obtiene datos debe tener un loading.tsx. Los usuarios nunca deben ver una pantalla en blanco mientras se cargan los datos.
4. Siempre proporciona error.tsx para cada segmento de ruta. Los errores ocurren. Atrapalos elegantemente con error.tsx. Incluye un botón de reintentar y una forma de navegar lejos.
"use client";
export default function Error({
error,
reset,
}: {
error: Error;
reset: () => void;
}) {
return (
<div>
<h2>Algo salió mal</h2>
<button onClick={reset}>Intentar de nuevo</button>
</div>
);
}5. Usa not-found.tsx para páginas 404 personalizadas. Llama a notFound() desde Componentes de Servidor cuando los datos no existen. Proporciona una página not-found útil por segmento de ruta.
6. Usa layouts para UI compartida, templates para el reseteo de estado por navegación. Los layouts persisten a través de navegaciones (bueno para barras laterales, nav). Los templates se remontan en cada navegación (bueno para animaciones, análisis por página).
7. Usa carpetas privadas (_components, _lib) para archivos que no son rutas. Cualquier cosa prefijada con _ está excluida del sistema de enrutamiento. Coloca helpers sin crear accidentalmente rutas.
8. Usa rutas paralelas (@slots) para layouts complejos. Los dashboards con paneles que se cargan independientemente, patrones modales y contenido condicional se benefician de rutas paralelas @slot con fallbacks default.tsx.
9. Usa rutas interceptadas para patrones modales. (.)photo/[id] intercepta la navegación para mostrar un modal mientras se preserva la URL compartible. La página completa se renderiza en navegación directa o recarga.
10. Mantén el middleware ágil. El middleware se ejecuta en cada solicitud. Solo úsalo para redirecciones de auth, detección de locale, encabezados de pruebas A/B y reescrituras de rutas. Nunca obtengas datos ni hagas cálculos pesados en middleware.
// middleware.ts - mantenlo rápido
export function middleware(request: NextRequest) {
const session = request.cookies.get("session");
if (!session && request.nextUrl.pathname.startsWith("/dashboard")) {
return NextResponse.redirect(new URL("/login", request.url));
}
}
export const config = {
matcher: ["/dashboard/:path*", "/settings/:path*"],
};Renderizado y Rendimiento (Reglas 11-20)
11. Por defecto, opta por renderizado estático. Las páginas sin datos dinámicos deben ser generadas estáticamente. Esta es la opción más rápida. Next.js hace esto automáticamente cuando no hay funciones dinámicas.
12. Entiende qué hace que una ruta sea dinámica. Usar cookies(), headers(), searchParams, o fetch con cache: "no-store" opta toda la ruta al renderizado dinámico. Sé intencional al respecto.
| Función | Efecto |
|---|---|
cookies() | Hace la ruta dinámica |
headers() | Hace la ruta dinámica |
searchParams | Hace la ruta dinámica |
fetch con cache: "no-store" | Hace la ruta dinámica |
unstable_noStore() | Hace la ruta dinámica |
13. Usa generateStaticParams para rutas dinámicas conocidas. Pre-renderiza páginas de productos, posts de blog y otro contenido con slugs conocidos en tiempo de compilación.
export async function generateStaticParams() {
const posts = await db.post.findMany({ select: { slug: true } });
return posts.map((post) => ({ slug: post.slug }));
}14. Usa ISR (revalidación) para contenido que cambia periódicamente. Establece revalidate para actualizar páginas en caché sin una reconstrucción completa.
// Revalida cada hora
export const revalidate = 3600;
// O por fetch
const data = await fetch(url, { next: { revalidate: 3600 } });15. Usa límites de Suspense estratégicamente. No envuelvas toda la página en un límite de Suspense. Envuelve cada sección que obtiene datos independientemente para que se transmitan independientemente.
16. Establece dimensiones de imagen o usa modo fill. Cada next/image debe tener width/height explícito o usar fill con un contenedor dimensionado. Esto previene Cambio de Diseño Acumulativo (CLS).
17. Usa priority en imágenes arriba del pliegue. La imagen hero y elemento LCP deben tener priority={true} para precarga inmediata.
18. Configura remotePatterns en lugar de domains. remotePatterns en next.config.ts te da control fino sobre fuentes de imágenes externas permitidas con coincidencia de protocolo y ruta.
19. Usa next/font para todas las fuentes. Las fuentes auto-hospedadas vía next/font eliminan cambios de diseño por carga de fuentes y evitan solicitudes de red externas a CDN de Google Fonts.
20. Habilita Turbopack para desarrollo. Agrega --turbopack a tu script dev. Es estable en Next.js 15+ y significativamente más rápido para HMR.
{
"scripts": {
"dev": "next dev --turbopack"
}
}Datos y Acciones de Servidor (Reglas 21-30)
21. Obtén datos en Componentes de Servidor, no en Componentes de Cliente. Los Componentes de Servidor tienen acceso directo a bases de datos, sistemas de archivos y APIs internas. No se necesita capa API.
22. Usa Server Actions para todas las mutaciones. Formularios, clics de botones y cualquier operación de escritura debe ir a través de Server Actions. Manejan protección CSRF, mejora progresiva y actualizaciones optimistas.
23. Valida entradas de Server Action con Zod. Nunca confíes en datos de formulario. Analiza y valida en el servidor antes de procesar.
"use server";
import { z } from "zod";
const schema = z.object({
title: z.string().min(1).max(200),
content: z.string().min(10),
});
export async function createPost(formData: FormData) {
const parsed = schema.safeParse(Object.fromEntries(formData));
if (!parsed.success) {
return { errors: parsed.error.flatten().fieldErrors };
}
await db.post.create({ data: parsed.data });
revalidatePath("/posts");
}24. Siempre llama a revalidatePath o revalidateTag después de mutaciones. La UI obsoleta después de una acción exitosa es un bug común. Revalida las rutas afectadas.
25. Usa Route Handlers (GET, POST) para endpoints de webhook y consumidores de API externa. Las Server Actions son para envíos de formulario internos. Los Route Handlers son para integraciones externas, webhooks y APIs REST.
26. Protege cada Server Action con autenticación. Las Server Actions son endpoints HTTP públicos. Verifica la sesión al inicio de cada acción.
"use server";
export async function deletePost(id: string) {
const session = await auth();
if (!session) throw new Error("Unauthorized");
if (session.user.role !== "admin") throw new Error("Forbidden");
await db.post.delete({ where: { id } });
revalidatePath("/posts");
}27. Usa revalidateTag para invalidación de caché de grano fino. Etiqueta tus fetches, luego invalida solo lo que cambió. Más eficiente que revalidatePath para relaciones de datos complejas.
// Obteniendo con etiquetas
const posts = await fetch(url, { next: { tags: ["posts"] } });
// Invalidando
revalidateTag("posts");28. Usa redirect en Server Actions para navegación post-mutación. Llama a redirect("/success") después de una acción exitosa. Lanza internamente, así que colócalo fuera del try/catch.
29. Maneja errores de Server Action con valores de retorno, no con errores lanzados. Devuelve { error: "message" } desde acciones y maneja en la UI con useActionState. Solo lanza para casos verdaderamente excepcionales.
30. Usa el paquete server-only para prevenir que código de servidor se filtre al cliente. Importa "server-only" al inicio de cualquier módulo que nunca debe ser empaquetado al lado del cliente (clientes de base de datos, claves secretas, etc.).
import "server-only";
import { db } from "./db";
export async function getSecretData() {
return db.secrets.findMany();
}Configuración e Implementación (Reglas 31-40)
31. Usa variables de entorno correctamente. Prefijo NEXT_PUBLIC_ solo para valores seguros de cliente. Todo lo demás es solo servidor. Valida variables env al inicio con Zod.
32. Configura next.config.ts mínimamente. Solo agrega configuración que necesitas. Evita características experimentales en producción a menos que estén completamente probadas.
33. Usa standalone output para implementaciones de Docker. output: "standalone" crea una compilación autocontenida con solo las dependencias necesarias.
// next.config.ts
const config = {
output: "standalone",
};
export default config;34. Establece encabezados de seguridad apropiados. Configura Content-Security-Policy, X-Frame-Options, X-Content-Type-Options y Referrer-Policy en next.config.ts o middleware.
35. Genera sitemap.xml y robots.txt dinámicamente. Usa app/sitemap.ts y app/robots.ts para generación dinámica basada en tu contenido.
36. Usa generateMetadata para páginas dinámicas. Metadatos estáticos para páginas fijas, función generateMetadata para páginas dinámicas (posts de blog, productos).
export async function generateMetadata({ params }: Props): Promise<Metadata> {
const { slug } = await params;
const post = await getPost(slug);
return {
title: post.title,
description: post.excerpt,
openGraph: { images: [post.coverImage] },
};
}37. Prueba con next build y next start antes de implementar. El modo desarrollo oculta muchos problemas. Siempre prueba en modo producción localmente.
38. Monitorea el tamaño del bundle. Usa @next/bundle-analyzer periódicamente para detectar crecimiento inesperado del bundle. Establece presupuestos para JavaScript del cliente.
39. Fija versiones de Next.js en producción. Usa versiones exactas ("next": "16.2.2") no rangos. Actualiza intencionalmente después de leer changelogs.
40. Usa instrumentation.ts para tareas de inicio del servidor. El calentamiento de conexión de base de datos, inicialización de telemetría y configuración única pertenecen al archivo de instrumentación.
// instrumentation.ts
export async function register() {
if (process.env.NEXT_RUNTIME === "nodejs") {
// Inicializa el pool de conexión de base de datos
// Configura rastreo de errores (Sentry, etc.)
}
}Preguntas Frecuentes
¿Cuál es la diferencia entre un layout y un template en Next.js?
- Layouts persisten a través de navegaciones -- buenos para barras laterales, barras de navegación y UI compartida
- Templates se remontan en cada navegación -- buenos para animaciones, análisis por página o reseteo de estado
¿Qué hace que una ruta sea dinámica en lugar de estática en Next.js?
Cualquiera de estos opta la ruta al renderizado dinámico:
- Llamar a
cookies()oheaders() - Leer
searchParams - Usar
fetchconcache: "no-store" - Llamar a
unstable_noStore()
¿Cómo pre-renderizas rutas dinámicas conocidas en tiempo de compilación?
export async function generateStaticParams() {
const posts = await db.post.findMany({ select: { slug: true } });
return posts.map((post) => ({ slug: post.slug }));
}¿Cuándo debes usar Route Handlers vs. Server Actions?
- Server Actions: envíos de formulario internos y mutaciones de UI desde tu propia aplicación
- Route Handlers: consumidores externos, webhooks (Stripe, GitHub), APIs REST para aplicaciones móviles, endpoints habilitados para CORS
Gotcha: ¿Por qué debes siempre llamar a revalidatePath o revalidateTag después de una mutación?
- Sin revalidación, la página/datos en caché permanecen obsoletos después de una Server Action exitosa
- Los usuarios verán contenido desactualizado aunque la mutación fue exitosa
- Este es uno de los bugs más comunes en aplicaciones Next.js
¿Cómo debes manejar errores devueltos desde Server Actions?
- Devuelve
{ error: "message" }desde la acción en lugar de lanzar - Maneja el error en la UI usando
useActionState - Solo lanza para casos verdaderamente excepcionales/inesperados
- Colocar
redirect()dentro del try/catch romperá porque redirect lanza internamente
¿Qué hace el paquete "server-only"?
import "server-only";- Importar esto al inicio de un módulo causa un error de compilación si ese módulo alguna vez se empaquet al cliente
- Úsalo para archivos que contienen clientes de base de datos, claves secretas o cualquier lógica exclusiva del servidor
Gotcha: ¿Qué pasa si envuelves toda tu página en un límite de Suspense único?
- Toda la página espera la fuente de datos más lenta antes de mostrar algo
- Las secciones independientes no pueden transmitirse conforme se resuelven
- En su lugar, envuelve cada sección que obtiene datos en su propio límite de Suspense
¿Cómo escribes el prop params para una página de ruta dinámica en TypeScript?
type Props = {
params: Promise<{ slug: string }>;
};
export default async function Page({ params }: Props) {
const { slug } = await params;
// ...
}
export async function generateMetadata({ params }: Props): Promise<Metadata> {
const { slug } = await params;
// ...
}¿Cómo escribes el componente error.tsx en TypeScript?
"use client";
export default function Error({
error,
reset,
}: {
error: Error & { digest?: string };
reset: () => void;
}) {
return <button onClick={reset}>Intentar de nuevo</button>;
}Nota: error.tsx debe ser un Componente de Cliente.
¿Cuál es el propósito de instrumentation.ts?
- Se ejecuta una vez al inicio del servidor antes de que se manejen solicitudes
- Úsalo para calentamiento de conexión de base de datos, inicialización de telemetría o configuración única
- Verifica
process.env.NEXT_RUNTIMEpara distinguir entre runtimes de Node.js y Edge
¿Por qué debes usar remotePatterns en lugar de domains para imágenes externas?
remotePatternspermite control fino con protocolo, nombre de host, puerto y coincidencia de rutadomainssolo coincide con el nombre de host y es menos seguroremotePatternspreviene cargar imágenes desde rutas inesperadas en un dominio permitido
¿Cómo debes validar variables de entorno al inicio?
Usa Zod para validar variables env para que la aplicación falle rápidamente con un error claro:
import { z } from "zod";
const envSchema = z.object({
DATABASE_URL: z.string().url(),
NEXT_PUBLIC_APP_URL: z.string().url(),
});
export const env = envSchema.parse(process.env);Referencia Rápida
| Categoría | Regla Clave |
|---|---|
| Enrutamiento | Una ruta, una responsabilidad. Siempre proporciona loading + error. |
| Renderizado | Estático por defecto. Sé intencional sobre dinámica. |
| Datos | Obtén del servidor por defecto. Server Actions para mutaciones. |
| Seguridad | Auth en cada acción. Valida todas las entradas. Nunca expongas secretos. |
| Rendimiento | Suspense por sección. Imágenes con prioridad. Turbopack en dev. |
| Implementación | Standalone output. Prueba con next build + next start. |