Next.js en producción
Úsalo para cualquier código en una aplicación Next.js 16 App Router. Las reglas específicas de seguridad (autenticación en acciones, CSP, cabeceras, taint) están en nextjs-security; las reglas de contenedor en docker-nextjs. La skill integrada de Claude Code next-best-practices cubre las convenciones generales de archivos, metadatos, optimización de imágenes y fuentes en profundidad; esta skill solo agrega lo específico de cómo construimos y publicamos.
Línea base: Next 16.4, React 19.3, Turbopack (predeterminado para dev y build), cacheComponents: true con partialPrefetching: true, output: "standalone", typedRoutes: true. El next.config.ts de la plantilla es la fuente de verdad.
Reglas
-
Componentes servidor por defecto;
"use client"solo para estado, efectos, APIs del navegador o manejadores de eventos. Pon la directiva en la hoja más pequeña que la necesite. No la agregues porque un archivo importa nuvui: las partes con estado de nuvui llevan su propia directiva (consulta la skillnuvui,rules/server-components.md).// Mal: toda la página es cliente por un solo manejador "use client"; export default function Page() { … <Select onValueChange={…} /> … } // Bien: la página permanece en el servidor, el manejador vive en una hoja export default async function Page() { const people = await getPeople(); return <PeopleFilter people={people} />; // people-filter.tsx tiene "use client" }Las props que cruzan a un componente cliente deben ser serializables: sin funciones (excepto Server Actions), sin instancias de clase.
-
Obtén datos en el servidor, en el componente que necesita los datos. Sin
useEffectpara obtener datos, sin manejador de ruta solo para alimentar tu propia página. Desduplica por solicitud concache()de React; almacena en caché entre solicitudes con la regla 3. -
Almacena en caché con
"use cache"+cacheLife+cacheTag; todo lo demás es dinámico. Con Componentes de Caché, nada se almacena en caché a menos que lo indiques, yfetchno tiene caché implícita.import { cacheLife, cacheTag } from "next/cache"; export async function getInvoice(id: InvoiceId) { "use cache"; cacheLife("minutes"); cacheTag(`invoice:${id}`); return db.invoice.findUnique({ where: { id } }); }No uses la configuración de segmento antigua (
export const dynamic,revalidate,fetchCache): da error bajo Componentes de Caché. Los argumentos y valores cerrados pasan a formar parte de la clave de caché, así que pasa ids, no objetos de solicitud completos. Detalles, los perfiles y"use cache: private": references/caching.md. -
Invalida con
updateTagen Server Actions,revalidateTag(tag, "max")en otros lugares.updateTag(tag)proporciona consistencia de lectura-después-de-escritura y solo funciona dentro de una Server Action. Desde un webhook o manejador de ruta usarevalidateTag(tag, "max")(stale-while-revalidate) orevalidateTag(tag, { expire: 0 }). La versión de un solo argumentorevalidateTag(tag)está obsoleta. -
Envuelve los datos en tiempo de solicitud en
<Suspense>, o la compilación falla.cookies(),headers(),searchParams,connection()y E/S no almacenada en caché fuera de un límite de Suspense detienen el prerenderizado con "datos no almacenados en caché o en tiempo de ejecución durante el prerenderizado". Asígnale un límite con un fallback esqueleto, almacena en caché, o (solo en el layout raíz, ver regla 14) marca la ruta como bloqueante conexport const instant = false. Todosparams,searchParams,cookies(),headers()son asíncronos: hazawaitde ellos. -
Cada Server Action valida su entrada y verifica la autorización por sí misma. Una Server Action es un endpoint POST público sin importar en qué página esté escrita. Analiza
FormDatacon el esquema detypescript-production(valibot), luego autoriza, luego muta, luegoupdateTag. Devuelve un resultado tipado para fallos esperados; lanza solo para errores. El conjunto completo de reglas está ennextjs-security."use server"; export async function renameProject(_: State, form: FormData): Promise<State> { const session = await requireSession(); // 401 si está ausente const input = v.safeParse(RenameProject, Object.fromEntries(form)); if (!input.success) return { ok: false, errors: flatten(input.issues) }; await assertCanEdit(session, input.output.projectId); // 403 si no está permitido await db.project.update(…); updateTag(`project:${input.output.projectId}`); return { ok: true }; } -
Los manejadores de ruta son para otros clientes (webhooks, móvil, salud), no para tus propias páginas. Ponlos bajo
app/api/. Valida el cuerpo como una acción, devuelveResponse.json(...), y establece códigos de estado intencionadamente. -
Cada aplicación tiene
GET /api/health, dinámico y sin dependencias. ElHEALTHCHECKde Docker y los probes de Kubernetes lo golpean. Debe responder sin tocar la base de datos, para que una dependencia lenta no reinicie cada pod.await connection()evita que se prerenderice en un archivo estático. Está en la plantilla ensrc/app/api/health/route.ts;proxy.tsexcluye/api/healthde su matcher. -
error.tsx,loading.tsxynot-found.tsxen la raíz, y por segmento donde la UX difiere.error.tsxes un componente cliente; registra a través deonRequestErrordeinstrumentation.ts, muestra un reintento conreset, nunca el mensaje de error. Llama anotFound()para registros faltantes en lugar de renderizar un estado vacío. -
NEXT_PUBLIC_*se hornea en la compilación; todo lo demás se lee en tiempo de ejecución en el servidor. Una imagen se promociona de main a producción sin cambios, por lo que un valorNEXT_PUBLIC_es el mismo en todos los entornos. Pon solo constantes de tiempo de compilación allí (nombre de la aplicación, un DSN público de Sentry que es el mismo en todas partes). La configuración por entorno (URLs de API, feature flags, secretos) es unprocess.env.Xdel lado del servidor, leído en un Componente Servidor o acción, y pasado al cliente como props si es necesario. Valida el entorno una vez ensrc/env.ts(valibot) e importa desde allí. Nunca uses la claveenvennext.config.ts: se incluye en los bundles del cliente. -
Marca los módulos solo de servidor con
import "server-only". Cualquier cosa que lea secretos, hable con la base de datos o importe un SDK con credenciales. Una importación de cliente de ello entonces falla la compilación en lugar de filtrar. -
after()para trabajo que no debería retrasar la respuesta (registros de auditoría, analíticas, calentamiento de caché). Leecookies()/headers()antes de llamar aafter, no dentro de él. Se ejecuta en el mismo proceso, por lo que el pod necesita su período de gracia SIGTERM (el chart estableceterminationGracePeriodSeconds). -
Instrumenta con
src/instrumentation.ts:registerOTel+onRequestError. OpenTelemetry a través de@vercel/otel, exportador configurado por las variables de entorno estándarOTEL_EXPORTER_OTLP_*en tiempo de ejecución. Consulta references/observability.md. -
Mantén
output: "standalone"y nunca importes desde fuera del workspace. El runner de Docker copia solo.next/standalone,.next/staticypublic. En el monorepo,server.jsaterriza en.next/standalone/apps/<app>/server.js; la raíz del workspace se encuentra desdepnpm-lock.yaml, así que no establezcasoutputFileTracingRoototurbopack.roota menos que la compilación advierta. Detalles de autoalojamiento (múltiples réplicas, claves de Server Action,deploymentId): references/self-hosting.md. -
El layout raíz lee el nonce CSP, por lo que la aplicación se renderiza por solicitud. El
proxy.tsde la plantilla establece un CSP basado en nonce ysrc/app/layout.tsxtieneexport const instant = false. Eso es un intercambio deliberado: los nonces y los shells estáticos prerenderizados no pueden coexistir, y para aplicaciones empresariales con inicio de sesión, un CSP estricto gana. El almacenamiento en caché de datos con"use cache"aún funciona. No elimines ninguna de las dos mitades sin leer la regla 4 denextjs-security. -
Estilos: SCSS BEM global, importado por el componente. Turbopack compila Sass con el paquete
sass;sassOptions.loadPaths: ["src/styles"]hace que@use "abstracts"funcione desde cualquier profundidad. Sin imports~, sinsassOptions.functions. Reglas enscss-bem. -
Imágenes a través de
next/image, fuentes a través denext/font.sharpviene con Next para el optimizador; la imagen del runner no necesita paquete adicional. Las imágenes remotas necesitanimages.remotePatternscon un nombre de host exacto. -
Verifica el tamaño del bundle con
pnpm next analyze(el analizador de Turbopack;@next/bundle-analyzeres solo para webpack). Carga bajo demanda los widgets pesados del cliente (editor nuvui, mapas, gráficos debajo del pliegue) connext/dynamic. -
Usa rutas tipadas y tipos generados.
typedRoutes: trueverifica<Link href>;PageProps<"/invoices/[id]">yLayoutPropsson globales después denext typegen(se ejecuta automáticamente connext dev/next build). No escribas manualmente los tiposparams. -
Sin
next lint. Fue eliminado en la versión 16. Biome lint (consultabiome); TypeScript verifica contsc --noEmitpor paquete.
Verificar
pnpm typecheck
pnpm lint
pnpm build # turbo run build: falla en errores de prerender, errores de tipo, errores de Sass
pnpm --filter <app> start & # luego:
curl -fsS localhost:3000/api/health
Fuentes
- https://nextjs.org/docs/app/guides/upgrading/version-16
- https://nextjs.org/docs/app/api-reference/config/next-config-js/cacheComponents
- https://nextjs.org/docs/app/guides/migrating-to-cache-components
- https://nextjs.org/docs/app/api-reference/directives/use-cache
- https://nextjs.org/docs/app/api-reference/functions/revalidateTag
- https://nextjs.org/docs/app/api-reference/functions/updateTag
- https://nextjs.org/docs/messages/blocking-prerender-dynamic
- https://nextjs.org/docs/app/api-reference/functions/after
- https://nextjs.org/docs/app/guides/open-telemetry
- https://nextjs.org/docs/app/api-reference/config/next-config-js/output
- https://nextjs.org/docs/app/guides/self-hosting
- https://nextjs.org/docs/app/guides/environment-variables
- https://nextjs.org/docs/app/guides/sass
- https://nextjs.org/docs/app/guides/package-bundling
- https://nextjs.org/docs/app/api-reference/file-conventions/proxy