Next.js 15 para SaaS: lo que pasó a la 16
Next.js 15 ya no es la versión mayor más nueva, y justamente por eso vale la pena escribir sobre ella. Las versiones que siguieron construyeron sobre sus decisiones en lugar de revertirlas, así que la arquitectura que un equipo de SaaS eligió en 2024 o 2025 es la misma que sigue corriendo en 2026. Este artículo trata de esas decisiones duraderas: qué cambió en 15, qué pasó a 16 y cómo darle forma a un producto multiinquilino encima de ellas para que siga siendo rápido mientras crece la lista de clientes. Nosotros construimos y operamos nuestros propios productos SaaS sobre este stack, entre ellos Passforge, una plataforma de preparación de exámenes con cobros propios, y DibsHub, un producto de reservas para proveedores independientes, así que lo que sigue viene de operar la cosa, no de leer el changelog.
Qué cambió Next.js 15 y qué sobrevivió
La versión 15 fue la entrega en la que el framework dejó de adivinar por ti. Sus cambios de mayor consecuencia fueron sobre valores por defecto, y los valores por defecto son lo que una base de código hereda durante años.
| Cambio en 15 | Estado después de 16 | Qué significa para una base de código SaaS |
|---|---|---|
fetch y los route handlers GET ya no se cachean por defecto | Se mantuvo, y se extendió a un modelo de caché explícito | Los datos del inquilino son dinámicos salvo que digas lo contrario; se acabaron las sorpresas de datos rancios |
Las API de petición (cookies(), headers(), params) pasaron a ser asíncronas | Se mantuvo; las formas síncronas se eliminaron | Cada verificación de auth y de inquilino tiene que hacer await; los codemods resuelven la parte mecánica |
| Soporte de React 19, Server Actions maduras | Se mantuvo; React 19 es la base | El manejo de formularios y las mutaciones corren en el servidor con entradas tipadas |
| Turbopack para desarrollo | Bundler por defecto en 16 para dev y build | Ciclos de retroalimentación más rápidos; la configuración específica de webpack necesita revisión al actualizar |
Prerenderizado parcial y use cache como experimentos | Formalizados en 16 bajo el modelo Cache Components | Cascarón estático con huecos dinámicos es ahora una forma de primera clase de construir páginas con sesión |
middleware.ts en el edge | Renombrado a proxy.ts en 16, mismo rol | Un renombrado con codemod; el enrutado por host y las redirecciones de auth viven en el mismo lugar |
El patrón es claro: nada de lo que introdujo 15 se revirtió. Si tu equipo aprendió entonces el modelo de servidor primero y caché explícita, ese conocimiento sigue vigente. Lo que sigue es cómo aplicarlo.
Server Components como la forma por defecto de una página SaaS
En el App Router, una página es un React Server Component a menos que un archivo opte por el navegador con "use client". Para un producto con sesión, ese valor por defecto es un regalo. Los datos se obtienen junto a la base de datos, los secretos nunca salen del servidor, y el navegador recibe HTML renderizado más solo el JavaScript que necesitan las partes interactivas. El modelo mental para un dashboard es: el layout autentica y resuelve el inquilino una vez, los server components debajo leen datos a través de ese contexto, y una capa delgada de client components maneja las partes que el usuario de verdad toca.
// app/(app)/layout.tsx (server component)
import { cookies } from "next/headers";
import { redirect } from "next/navigation";
import { resolveSession } from "@/lib/auth";
export default async function AppLayout({ children }) {
const session = await resolveSession(await cookies()); // asíncrono desde 15
if (!session) redirect("/login");
return <Shell user={session.user} tenant={session.tenant}>{children}</Shell>;
}
// app/(app)/invoices/page.tsx (server component, sin JS de cliente para la tabla)
import { withTenant } from "@/lib/tenant-db";
export default async function InvoicesPage() {
const session = await requireSession();
const invoices = await withTenant(session.tenantId, (db) => db.invoice.findMany());
return <InvoiceTable rows={invoices} />; // InvoiceTable es un client component
}Fíjate en que la verificación del inquilino es asunto del servidor en ambos archivos. El navegador nunca decide a qué inquilino pertenece; solo renderiza lo que el servidor ya filtró. Combina esto con aislamiento a nivel de base de datos y las dos capas se cubren mutuamente, que es el tema de nuestra pieza complementaria sobre seguridad a nivel de fila en Postgres.
Caché: di lo que quieres decir
Antes de 15, los resultados de fetch se cacheaban a menos que optaras por lo contrario, y muchos equipos sirvieron datos rancios de inquilinos antes de entender por qué. Desde 15 el valor por defecto se invirtió: nada se cachea hasta que lo pides. Es el valor por defecto más seguro para un producto donde cada petición pertenece a un cliente, y mueve la caché de accidente a decisión de diseño. La regla general que aplicamos en cada SaaS:
Cachea con fuerza la superficie de marketing, trata los datos de inquilino como dinámicos por defecto, y cachea deliberadamente lecturas caras concretas, etiquetadas, para que una mutación invalide exactamente lo que cambió.
Next.js 16 formalizó la mitad deliberada de esa regla con la directiva use cache y el modelo Cache Components. Una función o componente marcado con use cache se cachea con una vida útil y unas etiquetas que tú controlas; el resto del árbol sigue dinámico. La decisión de diseño importante para productos multiinquilino es que la clave de caché debe incluir al inquilino, y la forma más limpia de garantizarlo es pasar el id del inquilino como argumento para que forme parte de la clave automáticamente:
// lib/reports.ts
import { cacheTag, cacheLife } from "next/cache";
export async function monthlySummary(tenantId: string, month: string) {
"use cache";
cacheLife("hours"); // vida útil predefinida; ajústala por lectura
cacheTag(`summary:${tenantId}`); // etiqueta acotada al inquilino para invalidar
return withTenant(tenantId, (db) => db.$queryRaw`...agregado costoso...`);
}
// Después de una mutación que cambia las cifras:
import { revalidateTag } from "next/cache";
revalidateTag(`summary:${tenantId}`);Nunca cachees una función que lea el inquilino del contexto ambiental sin incluirlo también en los argumentos; así es como el reporte de un cliente termina en la pantalla de otro. Verifica los nombres exactos de la API contra las notas de la versión que uses, porque esta área evolucionó entre 15 y 16, pero el principio se sostiene en ambas: vida útil explícita, etiquetas explícitas, inquilino en la clave.
Streaming y Suspense: la parte que los usuarios sienten
Los productos densos en datos antes elegían entre un renderizado lento de página completa y una página llena de spinners. El streaming elimina esa elección. El servidor envía el cascarón de inmediato y transmite cada región lenta a medida que sus datos se resuelven, con <Suspense> marcando las fronteras. Navegación, barra lateral y encabezado aparecen al instante; la gráfica pesada se rellena un momento después, y la página nunca se bloquea por su consulta más lenta.
// app/(app)/dashboard/page.tsx
export default function Dashboard() {
return (
<>
<PageHeader title="Resumen" /> {/* instantáneo */}
<Suspense fallback={<KpiSkeleton />}>
<KpiRow /> {/* consulta rápida */}
</Suspense>
<Suspense fallback={<ChartSkeleton />}>
<RevenueChart /> {/* agregado lento */}
</Suspense>
</>
);
}Dos notas prácticas. Coloca fronteras alrededor de las regiones con costos de datos genuinamente distintos, no alrededor de cada componente; demasiadas fronteras producen una página que se arma a la vista en seis etapas. Y dale a cada fallback las mismas dimensiones que el contenido real para que el diseño no se desplace cuando lleguen los datos, que es tanto un detalle de experiencia de usuario como uno de Core Web Vitals.
Server Actions para tu propia interfaz, route handlers para todos los demás
Las mutaciones que antes exigían una ruta de API, un envoltorio de fetch y una capa de validación escrita a mano pueden ser una Server Action: una función marcada con "use server" que un formulario o un client component llama directamente. Auth, validación y la escritura en la base de datos viven en un solo lugar en el servidor.
// app/(app)/settings/actions.ts
"use server";
import { z } from "zod";
import { revalidatePath } from "next/cache";
const Input = z.object({ name: z.string().min(2).max(80) });
export async function renameWorkspace(formData: FormData) {
const session = await requireSession(); // nunca confíes en quien llama
const { name } = Input.parse(Object.fromEntries(formData));
await withTenant(session.tenantId, (db) =>
db.tenant.update({ where: { id: session.tenantId }, data: { name } })
);
revalidatePath("/settings");
}Trata cada action como un endpoint público, porque lo es: cualquiera con el id de la action puede llamarla con cualquier carga. Autentica dentro de la action, valida la entrada con un esquema y autoriza contra el inquilino, cada vez. Conserva los route handlers convencionales para lo que las actions no son: webhooks de proveedores de pago, una app móvil, integraciones de terceros y cualquier cosa que necesite un contrato HTTP estable y documentado.
Dónde viven auth e inquilinos: proxy, layout y capa de datos
Un SaaS tiene tres lugares naturales para aplicar el control de acceso, y una base de código sana usa los tres para trabajos distintos. El proxy (antes middleware) corre en el edge antes del renderizado: es el lugar correcto para decisiones gruesas como “sin cookie de sesión, redirigir al login”, enrutado por host para un dominio de administración separado y manejo de idioma. No debería tocar la base de datos. El layout resuelve la sesión y el inquilino una vez por petición y los pone a disposición del árbol; todo lo que necesite la identidad del usuario la lee aquí. La capa de datos aplica el acotamiento por inquilino en cada lectura y escritura, idealmente con la propia base de datos como respaldo. Cuando un equipo pone todo en el proxy, termina con llamadas a la base de datos en el edge y una puerta lenta y frágil; cuando pone todo en los componentes, termina con cien verificaciones dispersas. Tres capas, cada una con su trabajo, es la forma duradera.
Cascarón estático, huecos dinámicos: prerenderizado parcial para apps con sesión
La idea más interesante que salió de 15 y maduró en 16 es que una página puede ser mayormente estática y parcialmente dinámica al mismo tiempo. El armazón de un dashboard, su navegación, encabezados y estados vacíos, es idéntico para cada inquilino y puede prerenderizarse una vez en el build y servirse desde un CDN. Los datos dentro de él son dinámicos y llegan en streaming por petición. Cache Components es el mecanismo: marca las partes estáticas como cacheables, deja que las partes dinámicas lean datos de la petición, y el framework compone las dos en el edge. Para un SaaS esto significa que el primer pintado de una página con sesión es tan rápido como el de una página de marketing, mientras las cifras siguen siendo por cliente y frescas. Exige disciplina sobre qué componentes leen datos acotados a la petición, que es la disciplina que todo este artículo ha defendido de todos modos.
Despliegue: la misma arquitectura, tu infraestructura
Todo lo anterior funciona en Vercel y funciona autoalojado. El modo output: "standalone" del framework produce un servidor autocontenido que puedes correr bajo PM2 o en un contenedor detrás de Nginx, y así es como enviamos nuestros propios productos; el paso a paso está en nuestra guía para desplegar Next.js en una máquina Ubuntu normal. Los compromisos que hay que conocer: en tu propio servidor, la caché vive en el proceso o en un almacén que tú configuras, así que un despliegue con varias instancias necesita un manejador de caché compartido; la optimización de imágenes corre en tu CPU; y las funciones que asumen un runtime serverless o edge necesitan una comprobación contra tu runtime real. Nada de eso es motivo para evitar el autoalojamiento en un SaaS con tráfico predecible, y la diferencia de costo a escala es real. Es motivo para decidir temprano y probar la capa de caché en el entorno donde va a correr.
Pages Router: no reescribas por reescribir
El Pages Router sigue incluido, sigue recibiendo mantenimiento y sigue corriendo muchísimo software en producción, incluidas partes del nuestro. Si tienes un SaaS en Pages Router que está sano, la jugada correcta suele ser incremental: adopta el App Router para superficies nuevas, comparte la capa de datos y la librería de auth, y migra las rutas viejas cuando las toques por otros motivos. Una reescritura total para perseguir el router más nuevo gasta meses en trabajo que los clientes no pueden ver. La excepción es un producto cuyo dolor principal es exactamente lo que el App Router arregla, paquetes de cliente pesados y cascadas de obtención de datos en páginas densas; ahí, una migración planificada empezando por las peores páginas se paga rápido.
Lista de verificación para actualizar un producto existente
- Corre primero los codemods oficiales; resuelven las API de petición asíncronas y el renombrado a proxy.
- Audita cada
fetchy route handler que dependiera en silencio del viejo valor por defecto de caché, y añade caché explícita solo donde la quieras. - Busca cualquier cosa que lea el inquilino del contexto ambiental dentro de una función cacheada; pásalo como argumento.
- Saca del proxy las llamadas a la base de datos si alguna se coló.
- Añade una verificación de auth con validación de esquema a cada Server Action, y luego prueba una con una carga falsificada.
- Si autoalojas con más de una instancia, configura un manejador de caché compartido antes de activar
use cache. - Compila con Turbopack y arregla lo que dependiera de loaders específicos de webpack.
- Publica la actualización como su propia versión, no dentro de un sprint de funciones, para que una regresión tenga una causa obvia.
Un marco de decisión
- SaaS nuevo, desde cero: App Router, servidor primero, caché explícita, Server Actions para tu interfaz, route handlers para integraciones. Sin debate.
- Producto sano en Pages Router: quédate, adopta el App Router de forma incremental, comparte las capas de datos y auth.
- Dashboards densos que se sienten lentos: streaming con unas pocas fronteras bien puestas primero; es la gran victoria más barata.
- Clientes corporativos preguntando por aislamiento de datos: combina el contexto de inquilino del lado del servidor con RLS en la base de datos y sé capaz de mostrar ambos.
- Funciones en tiempo real en el roadmap: mantenlas en un canal dedicado en lugar de forzarlas por el modelo de renderizado; mira nuestra pieza sobre WebSockets y datos en vivo.
- Inicio de sesión sin contraseña como diferenciador: encaja de forma natural en el modelo de sesión a nivel de layout; mira passkeys.
El Next.js moderno premia a los equipos que se comprometen con el modelo de servidor primero y castiga a los que lo pelean con viejos hábitos. El número de versión importa menos cada año; la arquitectura importa más. Si quieres un socio que haya lanzado y opere SaaS sobre este stack, mira cómo abordamos el desarrollo de SaaS y las aplicaciones web, o cuéntanos qué estás construyendo.
Preguntas frecuentes
¿Next.js 15 sigue siendo relevante ahora que existe Next.js 16?
Sí, porque la 16 construyó sobre las decisiones de la 15 en lugar de revertirlas. El App Router con servidor primero, el valor por defecto invertido de caché donde nada se cachea hasta que lo pides, las API de petición asíncronas, React 19 y las Server Actions siguieron adelante. La 16 formalizó las piezas experimentales, en especial la directiva use cache bajo el modelo Cache Components, hizo de Turbopack el bundler por defecto y renombró middleware a proxy. Un equipo que aprendió el modelo de la 15 está al día; la actualización es sobre todo codemods más una auditoría de caché.
¿Un SaaS debería usar el App Router o el Pages Router?
Un SaaS nuevo debería empezar en el App Router: server components por defecto, streaming con Suspense, Server Actions para su propia interfaz y route handlers para integraciones. Un producto sano que ya está en Pages Router no debería reescribirse por completo; adopta el App Router para superficies nuevas, comparte las capas de datos y auth, y migra las rutas viejas cuando las toques por otros motivos. La excepción es un producto cuyo dolor principal son paquetes de cliente pesados y cascadas de obtención de datos en páginas densas, donde migrar primero las peores páginas se paga rápido.
¿Cómo funciona la caché en Next.js 15 y 16 con datos multiinquilino?
Desde la 15, fetch y los route handlers GET no se cachean a menos que lo pidas, así que los datos de inquilino son dinámicos por defecto. Cuando sí cachees una lectura cara con use cache, dale una vida útil y una etiqueta explícitas, y pasa el id del inquilino como argumento para que forme parte de la clave de caché; nunca dejes que una función cacheada lea el inquilino del contexto ambiental. Después de una mutación, revalida la etiqueta acotada al inquilino. En un despliegue autoalojado con más de una instancia, configura un manejador de caché compartido antes de activarlo.
¿Las Server Actions son lo bastante seguras para un SaaS?
Lo son, siempre que trates cada action como un endpoint público, porque eso es lo que es. Autentica dentro de la action, valida la entrada con un esquema y autoriza la operación contra el inquilino de quien llama en cada invocación. Conserva los route handlers convencionales para webhooks, clientes móviles e integraciones de terceros que necesiten un contrato HTTP estable y documentado. Las actions eliminan mucho código de pegamento para tu propia interfaz sin cambiar el modelo de seguridad.
¿Dónde deberían vivir la autenticación y las verificaciones de inquilino en una app Next.js?
En tres capas, cada una con un trabajo. El proxy (antes middleware) toma decisiones gruesas en el edge, como redirigir una petición sin cookie de sesión, el enrutado por host y el manejo de idioma, y nunca debería tocar la base de datos. El layout raíz resuelve la sesión y el inquilino una vez por petición para todo el árbol. La capa de datos acota cada lectura y escritura a ese inquilino, idealmente con Row-Level Security de Postgres como respaldo para que un filtro olvidado no pueda filtrar datos.
¿Puedo autoalojar un SaaS en Next.js en lugar de usar Vercel?
Sí. El modo de salida standalone produce un servidor autocontenido que puedes correr bajo PM2 o en un contenedor detrás de Nginx, y toda la arquitectura de servidor primero funciona ahí. Conoce los compromisos: la caché vive en el proceso o en un almacén que configuras, la optimización de imágenes corre en tu CPU, y cualquier función que asuma un runtime serverless o edge necesita verificarse contra tu entorno real. Para tráfico SaaS predecible la diferencia de costo a escala es real, así que decide temprano y prueba la capa de caché donde va a correr.
¿Estás pensando en construir esto?
Appluex diseña y lanza apps móviles y web en producción, incluidas funciones de inteligencia artificial. Hablemos.