Next.js 15: Todas las Novedades que Cambian tu Forma de Programar
Guía completa de Next.js 15: React 19, caché por defecto desactivado, turbopack estable, after(), mejoras en Server Actions y todo lo que necesitas saber para migrar.
Tabla de contenidos
Next.js 15 salió con cambios que rompen muchas apps y otros que hacen la vida mucho mejor. Voy a explicar cada uno con código real para que sepas exactamente qué te afecta.
Los cambios más importantes de un vistazo
- Caché desactivado por defecto — el cambio que más rompe cosas
- React 19 integrado —
use(), Server Actions mejorados, optimistic updates - Turbopack estable en desarrollo — 76% más rápido en dev
after()— ejecutar código después de enviar la respuestaforbidden()yunauthorized()— manejo nativo de 401/403instrumentation.jsestable — observabilidad en el servidor- Mejoras en Forms —
<Form>component nativo
1. El caché ya no está activo por defecto
Este es el cambio que más confunde a la gente que migra desde v14.
¿Cómo era en v14?
// Next.js 14 — esto se CACHEABA por defecto (force-cache)
const res = await fetch('https://api.example.com/productos')
const data = await res.json()
// ⚠️ Solo hacía la petición UNA VEZ y devolvía el resultado cacheado el resto
¿Cómo es en v15?
// Next.js 15 — NO se cachea por defecto (no-store)
const res = await fetch('https://api.example.com/productos')
const data = await res.json()
// ✅ Hace la petición CADA VEZ que se renderiza el componente
Ahora el comportamiento por defecto es el que esperas de cualquier fetch normal. Si quieres caché, tienes que pedirla explícitamente:
// Caché estático: igual que v14 por defecto
const res = await fetch('https://api.example.com/productos', {
cache: 'force-cache'
})
// Revalidar cada hora
const res = await fetch('https://api.example.com/productos', {
next: { revalidate: 3600 }
})
// Sin caché (ahora el default)
const res = await fetch('https://api.example.com/productos', {
cache: 'no-store'
})
Route Handlers también cambian
// v14 — GET Route Handler se CACHEABA
// v15 — GET Route Handler NO se cachea por defecto
// app/api/productos/route.ts
export async function GET() {
const productos = await db.query('SELECT * FROM productos')
return Response.json(productos)
// En v15: siempre fresh. En v14: podía quedar stale.
}
// Para recuperar el comportamiento de v14 en v15:
export const dynamic = 'force-static'
export async function GET() { ... }
Client Router Cache
// v14 — los layouts se cacheaban 5 minutos en el cliente
// v15 — el Client Router Cache no cachea por defecto
// Recuperar comportamiento v14 en next.config.ts:
const config = {
experimental: {
staleTimes: {
dynamic: 30, // segundos
static: 180,
}
}
}
2. React 19 — Las novedades que más usarás
El hook use() — leer Promises y Context
// Antes (React 18) — suspense con async component
async function Productos() {
const productos = await fetchProductos() // Solo funciona en Server Components
return <Lista items={productos} />
}
// React 19 — use() funciona en Client Components también
import { use } from 'react'
function Productos({ productosPromise }: { productosPromise: Promise<Producto[]> }) {
const productos = use(productosPromise) // Suspende automáticamente
return <Lista items={productos} />
}
// Uso:
const promesa = fetchProductos()
<Suspense fallback={<Skeleton />}>
<Productos productosPromise={promesa} />
</Suspense>
También funciona con Context (reemplaza useContext):
const tema = use(TemaContext) // equivalente a useContext(TemaContext) pero más flexible
Server Actions mejorados con useActionState
// React 19 + Next.js 15
import { useActionState } from 'react'
async function crearProducto(estado: Estado, formData: FormData) {
'use server'
const nombre = formData.get('nombre') as string
try {
await db.insert({ nombre })
return { exito: true, mensaje: 'Producto creado' }
} catch (e) {
return { exito: false, error: 'Error al crear' }
}
}
function FormularioProducto() {
const [estado, accion, isPending] = useActionState(crearProducto, null)
return (
<form action={accion}>
<input name="nombre" type="text" required />
<button disabled={isPending}>
{isPending ? 'Guardando...' : 'Crear'}
</button>
{estado?.error && <p className="text-red-500">{estado.error}</p>}
{estado?.exito && <p className="text-green-500">{estado.mensaje}</p>}
</form>
)
}
useOptimistic — updates optimistas limpios
import { useOptimistic } from 'react'
function ListaTareas({ tareas }: { tareas: Tarea[] }) {
const [tareasOptimistas, addTareaOptimista] = useOptimistic(
tareas,
(estado, nuevaTarea: Tarea) => [...estado, nuevaTarea]
)
async function añadirTarea(formData: FormData) {
const texto = formData.get('texto') as string
const tareaTemp = { id: Date.now(), texto, completada: false }
addTareaOptimista(tareaTemp) // UI actualiza inmediatamente
await crearTarea(texto) // Luego hace la llamada real
}
return (
<div>
{tareasOptimistas.map(t => <TareaItem key={t.id} tarea={t} />)}
<form action={añadirTarea}>
<input name="texto" />
<button>Añadir</button>
</form>
</div>
)
}
3. Turbopack estable en desarrollo
# Next.js 15 — activar Turbopack en desarrollo
next dev --turbopack
O en package.json:
{
"scripts": {
"dev": "next dev --turbopack"
}
}
Los números que publica Vercel en proyectos grandes:
- Arranque inicial del servidor: 76% más rápido
- Hot Module Replacement (HMR): hasta 96% más rápido
- Compilación de rutas: significativamente más rápido
Para proyectos medianos/grandes, el cambio en experiencia de desarrollo es muy notable. Para proyectos pequeños la diferencia es menos perceptible.
Compatibilidad: La mayoría de loaders de webpack tienen equivalente en Turbopack, pero algunos plugins personalizados aún no son compatibles. Revisa tu next.config.ts antes de activarlo.
4. after() — Código después de la respuesta
Una función muy útil que no existía antes de forma oficial:
import { after } from 'next/server'
export async function GET() {
const datos = await fetchDatos()
// Envía la respuesta al usuario
const respuesta = Response.json(datos)
// Este código se ejecuta DESPUÉS de enviar la respuesta
// El usuario ya tiene su respuesta, no espera esto
after(async () => {
await analytics.track('api_called')
await cache.actualizar('datos')
await logger.guardar({ timestamp: new Date(), ruta: '/api/datos' })
})
return respuesta
}
Perfecto para:
- Analytics y logging sin penalizar la latencia
- Invalidar cachés
- Enviar notificaciones
- Tareas de mantenimiento
Funciona en Route Handlers, Server Components y Server Actions.
5. forbidden() y unauthorized() — Errores 401/403 nativos
import { forbidden, unauthorized } from 'next/navigation'
// En un Server Component o Route Handler
export default async function PaginaAdmin() {
const sesion = await getSession()
if (!sesion) {
unauthorized() // Lanza un 401, renderiza unauthorized.tsx
}
if (sesion.rol !== 'admin') {
forbidden() // Lanza un 403, renderiza forbidden.tsx
}
return <AdminPanel />
}
Crea los archivos de error en app/:
app/
unauthorized.tsx ← se renderiza con 401
forbidden.tsx ← se renderiza con 403
not-found.tsx ← 404 (existía antes)
// app/unauthorized.tsx
export default function NoAutorizado() {
return (
<div>
<h1>Inicia sesión para continuar</h1>
<a href="/login">Iniciar sesión</a>
</div>
)
}
6. El componente <Form> de Next.js
Next.js 15 añade un componente <Form> que extiende el <form> de HTML:
import Form from 'next/form'
function BuscadorProductos() {
return (
<Form action="/buscar">
<input name="q" placeholder="Buscar..." />
<button type="submit">Buscar</button>
</Form>
)
}
Lo que hace diferente:
- Prefetch automático de la página
/buscarcuando el formulario es visible - Navegación del lado cliente (sin recarga) usando el App Router
- Loading UI automático (usa
loading.tsx) - URL actualizada con los params del form para poder compartir el resultado
Cómo migrar de Next.js 14 a 15
Usa el codemod automático
npx @next/codemod@canary upgrade latest
Esto actualiza las dependencias y aplica los cambios de API automáticamente.
Cambios manuales que el codemod no hace
1. Revisar los fetch con caché implícita:
# Busca en tu proyecto todos los fetch sin cache explícito
grep -rn "await fetch(" src/ --include="*.tsx" --include="*.ts"
Para cada fetch en un Server Component, decide si quieres caché o no y añádelo explícitamente.
2. cookies(), headers(), params ahora son async:
// v14
import { cookies } from 'next/headers'
const cookieStore = cookies()
const token = cookieStore.get('token')
// v15
import { cookies } from 'next/headers'
const cookieStore = await cookies() // ← await obligatorio
const token = cookieStore.get('token')
El codemod lo migra automáticamente, pero si tienes abstracciones propias encima, revísalas.
3. params en Page components ahora es una Promise:
// v14
export default function Pagina({ params }: { params: { slug: string } }) {
const { slug } = params
}
// v15
export default async function Pagina({ params }: { params: Promise<{ slug: string }> }) {
const { slug } = await params // ← await
}
Resumen de lo que afecta a tu código del día a día
| Feature | Impacto en migración | Lo que tienes que hacer |
|---|---|---|
| Caché desactivado | Alto | Revisar todos los fetch y añadir cache explícito |
cookies() async | Alto | El codemod lo migra, revisar abstracciones |
params async | Medio | El codemod lo migra |
| React 19 | Bajo | Sin cambios breaking para la mayoría |
| Turbopack | Bajo | Opt-in, añadir --turbopack en dev |
after() | Ninguno | Nueva API, úsala cuando quieras |
forbidden()/unauthorized() | Ninguno | Reemplaza tu manejo manual de errores |
Para proyectos nuevos, Next.js 15 con React 19 es la combinación a usar en 2026. Para migrar proyectos existentes: ejecuta el codemod, revisa los fetch y reserva 2-4 horas para los casos edge.