Nuxt 4: La Guía Definitiva y Completa (2026) - Español
Nuxt 4 fue lanzado oficialmente el 15 de julio de 2025. Esta es la guía más completa que encontrarás: desde instalación desde cero hasta patrones avanzados, buenas prácticas, configuración del proyecto y todo lo que cambió respecto a Nuxt 3. Si estás empezando en Nuxt 4, este es tu punto de partida.
Table of Contents
- ¿Qué es Nuxt 4 y qué cambió?
- Instalación y Primer Proyecto
- La Nueva Estructura de Carpetas
- Configuración del Proyecto: nuxt.config.ts
- Routing y Páginas
- Layouts y Componentes
- Data Fetching: useFetch y useAsyncData
- Composables y Estado Global
- El Servidor: Nitro y API Routes
- La Carpeta shared/
- TypeScript en Nuxt 4
- Plugins y Middleware
- SEO y Head Management
- Nuxt Modules Esenciales
- Nuxt UI v4: El Design System Oficial
- Deployment y Estrategias de Rendering
- Migrar de Nuxt 3 a Nuxt 4
- Buenas Prácticas y Patrones Avanzados
- Nuxt DevTools
- Qué viene: Nuxt 5
1. ¿Qué es Nuxt 4 y qué cambió?
Nuxt es el meta-framework full-stack de Vue.js por excelencia. Piénsalo como Next.js pero para Vue: SSR, SSG, ISR, auto-imports, file-based routing, server API routes y mucho más, todo configurado de fábrica.
Nuxt 4, lanzado en julio de 2025, no es una revolución — es una evolución inteligente. A diferencia del salto de Nuxt 2 a 3 (que fue casi una reescritura completa), Nuxt 4 es una versión estable, enfocada en DX (Developer Experience) con mejoras concretas en:
┌─────────────────────────────────────────────────────────────────┐
│ NUXT 4 — LO NUEVO │
│ │
│ 🗂️ Nueva estructura app/ — código separado de config │
│ 🔄 Data Fetching mejorado — caché, deduplication, cleanup │
│ 🔧 TypeScript multi-proyecto — mejor autocomplete │
│ ⚡ CLI más rápido — sockets, compile cache, fs.watch │
│ 🏷️ Nombres de componentes consistentes — DevTools + KeepAlive │
│ 📄 Unhead v2 — mejor gestión del <head> │
│ 🧹 Limpieza de APIs deprecadas │
│ │
│ Versión actual: 4.3.1 (noviembre 2025) │
│ Lanzamiento: 15 de julio de 2025 │
└─────────────────────────────────────────────────────────────────┘
Nuxt 4 vs Nuxt 3 — Resumen visual
| Característica | Nuxt 3 | Nuxt 4 |
|---|---|---|
| Estructura de carpetas | Raíz del proyecto | app/ (separado) |
| Data fetching key | Puede duplicar refs | Una sola ref por key |
| TypeScript config | Un tsconfig para todo | Multi-proyecto separado |
| Valores por defecto (data) | null | undefined |
| CLI comunicación | Puertos de red | Sockets internos |
| Estilos inline | CSS global + componentes | Solo componentes Vue |
| Nuxt 2 compat en @nuxt/kit | ✅ | ❌ (eliminado) |
| Unhead | v1 | v2 |
2. Instalación y Primer Proyecto
Prerrequisitos
# Necesitas:
# - Node.js 18.0.0 o superior (recomendado: Node 20 LTS)
# - Un gestor de paquetes: npm, yarn, pnpm o bun
node --version # v20.x.x ✅
pnpm --version # 9.x ✅ (recomendado)
Recomendación: Usa pnpm o bun. Son significativamente más rápidos que npm para proyectos Nuxt.
Crear un proyecto nuevo
# Con npx (npm)
npx nuxi@latest init mi-proyecto
# Con pnpm (recomendado)
pnpm dlx nuxi@latest init mi-proyecto
# Con bun
bunx nuxi@latest init mi-proyecto
El CLI te hará algunas preguntas:
? Which package manager would you like to use?
❯ pnpm
npm
yarn
bun
? Would you like to initialize a git repository?
❯ Yes
No
? Which modules would you like to use? (space to toggle)
◯ @nuxt/ui
◯ @nuxt/eslint
◯ @nuxt/fonts
◯ @nuxt/image
Estructura inicial generada
cd mi-proyecto
mi-proyecto/
├── app/ ← ¡NUEVO en Nuxt 4! Todo tu código aquí
│ ├── assets/
│ ├── components/
│ ├── composables/
│ ├── layouts/
│ ├── middleware/
│ ├── pages/
│ ├── plugins/
│ ├── utils/
│ ├── app.vue ← Entry point de la app
│ ├── app.config.ts ← Config runtime (UI themes, etc.)
│ └── error.vue ← Página de error personalizada
├── public/ ← Archivos estáticos (favicon, robots.txt)
├── server/ ← API routes, middleware de servidor
│ ├── api/
│ ├── middleware/
│ └── utils/
├── shared/ ← Código compartido client/server
│ ├── types/
│ └── utils/
├── .nuxt/ ← Generado automáticamente (no tocar)
├── nuxt.config.ts ← Configuración principal
├── package.json
└── tsconfig.json
Arrancar el servidor de desarrollo
pnpm dev
# → http://localhost:3000
Verás algo como:
NUXT v4.3.1
➜ Local: http://localhost:3000/
➜ Network: http://192.168.1.x:3000/
➜ DevTools: http://localhost:3000/__nuxt_devtools__/
✔ Vite client built in 321ms
✔ Vite server built in 412ms
✔ Nuxt Nitro server built in 890ms
Comandos esenciales
pnpm dev # Servidor de desarrollo
pnpm build # Build para producción
pnpm preview # Preview del build de producción
pnpm generate # Generación estática (SSG)
pnpm typecheck # Verificar tipos TypeScript
# Nuxi CLI utilities
npx nuxi add page about # Crea app/pages/about.vue
npx nuxi add component Header # Crea app/components/Header.vue
npx nuxi add composable useUser # Crea app/composables/useUser.ts
npx nuxi add layout admin # Crea app/layouts/admin.vue
npx nuxi add api users # Crea server/api/users.ts
npx nuxi upgrade # Actualizar Nuxt
3. La Nueva Estructura de Carpetas
Este es el cambio más visible de Nuxt 4. Toda la lógica de la aplicación vive dentro de app/.
¿Por qué app/?
Problema en Nuxt 3:
mi-proyecto/
├── components/ ← Tu código
├── pages/ ← Tu código
├── node_modules/ ← Dependencias (100k+ archivos)
├── .git/ ← Git metadata
├── .nuxt/ ← Build cache
El file watcher (Chokidar) tenía que vigilar TODO el directorio raíz,
incluyendo node_modules y .git.
En proyectos grandes esto era lento, especialmente en Windows y Linux.
Solución en Nuxt 4:
mi-proyecto/
├── app/ ← EL WATCHER SOLO VIGILA ESTO ✅
│ ├── components/
│ └── pages/
├── node_modules/ ← Ignorado
├── .git/ ← Ignorado
Resultado: Startups de desarrollo más rápidos, mejor rendimiento del IDE, y separación conceptual clara entre código de app, código de servidor y configuración.
Cada carpeta y su propósito
app/ — Código del cliente (front-end)
app/
├── assets/ ← CSS global, imágenes, fuentes procesadas por Vite
├── components/ ← Componentes Vue (auto-importados)
├── composables/ ← Composables reutilizables (auto-importados)
├── layouts/ ← Layouts de página
├── middleware/ ← Middleware de navegación (cliente)
├── pages/ ← File-based routing
├── plugins/ ← Plugins que se ejecutan al iniciar la app
├── utils/ ← Funciones de utilidad (auto-importadas)
├── app.vue ← Root component
├── app.config.ts ← Config accesible en runtime
└── error.vue ← Página de error personalizada
server/ — Código del servidor (Nitro)
server/
├── api/ ← Endpoints de API (/api/...)
├── middleware/ ← Middleware HTTP del servidor
├── plugins/ ← Plugins de Nitro
├── routes/ ← Rutas de servidor adicionales
└── utils/ ← Utilidades del servidor (auto-importadas)
shared/ — Código compartido (cliente + servidor)
shared/
├── types/ ← Tipos TypeScript compartidos
└── utils/ ← Funciones puras usables en ambos contextos
Raíz del proyecto — Solo configuración
nuxt.config.ts ← Configuración de Nuxt
tsconfig.json ← TypeScript config
package.json ← Dependencias
.env ← Variables de entorno
public/ ← Archivos estáticos sin procesar
content/ ← Contenido para @nuxt/content (si lo usas)
Compatibilidad con Nuxt 3 (sin migrar)
¿Tienes un proyecto Nuxt 3 y no quieres migrar? No pasa nada. Nuxt 4 detecta automáticamente si tienes la estructura antigua y la respeta:
// nuxt.config.ts — Para mantener estructura Nuxt 3
export default defineNuxtConfig({
// Si NO tienes app/, Nuxt usa la estructura raíz automáticamente
// No necesitas hacer nada
})
4. Configuración del Proyecto: nuxt.config.ts
El archivo nuxt.config.ts es el corazón de la configuración. Con Nuxt 4, hay nuevas opciones y algunas que desaparecieron.
Configuración base completa
// nuxt.config.ts
export default defineNuxtConfig({
// ┌─────────────────────────────────────────────┐
// │ INFORMACIÓN DEL PROYECTO │
// └─────────────────────────────────────────────┘
// Compatibilidad con versiones
compatibilityDate: '2025-07-15', // Fecha de release de Nuxt 4
// Habilitar DevTools
devtools: { enabled: true },
// ┌─────────────────────────────────────────────┐
// │ MÓDULOS │
// └─────────────────────────────────────────────┘
modules: [
'@nuxt/ui', // UI components
'@nuxt/eslint', // ESLint integrado
'@nuxt/fonts', // Gestión de fuentes
'@nuxt/image', // Optimización de imágenes
'@nuxt/content', // CMS basado en archivos
'@nuxtjs/i18n', // Internacionalización
'nuxt-icon', // Iconos
],
// ┌─────────────────────────────────────────────┐
// │ CSS │
// └─────────────────────────────────────────────┘
css: ['~/assets/css/main.css'],
// ┌─────────────────────────────────────────────┐
// │ RUNTIME CONFIG (variables de entorno) │
// └─────────────────────────────────────────────┘
runtimeConfig: {
// Solo accesible en el servidor
databaseUrl: process.env.DATABASE_URL,
jwtSecret: process.env.JWT_SECRET,
stripeSecretKey: process.env.STRIPE_SECRET_KEY,
// Accesible también en el cliente (public)
public: {
apiBase: process.env.NUXT_PUBLIC_API_BASE || '/api',
siteUrl: process.env.NUXT_PUBLIC_SITE_URL || 'http://localhost:3000',
stripePublicKey: process.env.STRIPE_PUBLIC_KEY,
}
},
// ┌─────────────────────────────────────────────┐
// │ APP CONFIG (no sensible, UI/theme) │
// └─────────────────────────────────────────────┘
// Lo que va aquí también puede estar en app/app.config.ts
// (app.config.ts es editable en runtime sin rebuild)
// ┌─────────────────────────────────────────────┐
// │ NITRO (servidor) │
// └─────────────────────────────────────────────┘
nitro: {
preset: 'node-server', // 'vercel', 'cloudflare', 'netlify', etc.
routeRules: {
// Static pages (pre-renderizadas)
'/': { prerender: true },
'/about': { prerender: true },
// ISR — regeneración incremental (1 hora)
'/blog/**': { isr: 3600 },
// SWR — stale while revalidate
'/api/products/**': { swr: 600 },
// Solo SSR
'/dashboard/**': { ssr: true },
// Deshabilitar SSR (SPA mode)
'/app/**': { ssr: false },
// Añadir headers
'/api/**': {
headers: { 'cache-control': 's-maxage=0' },
cors: true,
},
},
},
// ┌─────────────────────────────────────────────┐
// │ VITE │
// └─────────────────────────────────────────────┘
vite: {
define: {
__APP_VERSION__: JSON.stringify(process.env.npm_package_version),
},
// Optimizaciones
optimizeDeps: {
include: ['lodash-es'],
},
},
// ┌─────────────────────────────────────────────┐
// │ EXPERIMENTAL (Nuxt 4.x) │
// └─────────────────────────────────────────────┘
experimental: {
// Extrae handlers de useAsyncData en chunks separados
// Reduce bundle size hasta un 39% en sitios estáticos
asyncDataHandler: true, // Disponible en Nuxt 4.2+
// TypeScript plugin para mejor autocomplete (Nuxt 4.2+)
typescriptPlugin: true,
// Pagado por adelantado de rendering
headNext: true,
},
// ┌─────────────────────────────────────────────┐
// │ TYPESCRIPT │
// └─────────────────────────────────────────────┘
typescript: {
strict: true, // Habilita strict mode
typeCheck: true, // Typechecking en build (lento pero seguro)
},
})
Variables de entorno (.env)
# .env
# Variables NUXT_PUBLIC_* se exponen al cliente automáticamente
NUXT_PUBLIC_API_BASE=https://api.ejemplo.com
NUXT_PUBLIC_SITE_URL=https://ejemplo.com
# Variables sin NUXT_PUBLIC_ son solo del servidor
DATABASE_URL=postgresql://user:pass@localhost:5432/mydb
JWT_SECRET=mi-secreto-super-seguro-aqui
STRIPE_SECRET_KEY=sk_live_...
STRIPE_PUBLIC_KEY=pk_live_...
// Usar en componentes (cliente + servidor)
const config = useRuntimeConfig()
console.log(config.public.apiBase) // Disponible siempre
// Solo en servidor (API routes, server middleware)
console.log(config.databaseUrl) // Undefined en el cliente ✅
5. Routing y Páginas
Nuxt usa file-based routing: la estructura de archivos en app/pages/ define automáticamente las rutas.
Rutas básicas
app/pages/
├── index.vue → /
├── about.vue → /about
├── contact.vue → /contact
└── blog/
├── index.vue → /blog
└── [slug].vue → /blog/:slug (ruta dinámica)
Página básica
<!-- app/pages/index.vue -->
<template>
<div>
<h1>Bienvenido a mi app</h1>
<p>Esta es la página principal.</p>
<!-- NuxtLink es como RouterLink pero con prefetching automático -->
<NuxtLink to="/about">Ir a About</NuxtLink>
</div>
</template>
<script setup lang="ts">
// definePageMeta — configurar comportamiento de la página
definePageMeta({
title: 'Inicio',
layout: 'default',
middleware: ['auth'], // Middleware de autenticación
keepalive: true, // Mantener componente vivo en navegación
})
</script>
Rutas dinámicas
<!-- app/pages/blog/[slug].vue -->
<template>
<article>
<h1>{{ post?.title }}</h1>
<p>{{ post?.body }}</p>
</article>
</template>
<script setup lang="ts">
// Acceder a parámetros de ruta
const route = useRoute()
const slug = route.params.slug as string
// Fetch del post
const { data: post } = await useFetch(`/api/posts/${slug}`)
// SEO dinámico basado en datos del post
useSeoMeta({
title: () => post.value?.title,
description: () => post.value?.excerpt,
})
</script>
Rutas con parámetros múltiples y anidadas
app/pages/
├── users/
│ ├── index.vue → /users
│ ├── [id].vue → /users/:id
│ └── [id]/
│ ├── profile.vue → /users/:id/profile
│ └── settings.vue → /users/:id/settings
├── [...slug].vue → Catch-all (cualquier ruta no definida)
└── [[optional]].vue → Parámetro opcional
Navegación programática
const router = useRouter()
const route = useRoute()
// Navegar
router.push('/dashboard')
router.push({ name: 'user-id', params: { id: '123' } })
router.replace('/login')
router.back()
// Obtener info de ruta actual
console.log(route.path) // '/blog/mi-post'
console.log(route.params) // { slug: 'mi-post' }
console.log(route.query) // { page: '2' }
console.log(route.fullPath) // '/blog/mi-post?page=2'
Route Rules en nuxt.config.ts (Híbrido SSR/SSG)
Una de las funciones más poderosas de Nuxt: mezclar estrategias de rendering por ruta.
// nuxt.config.ts
export default defineNuxtConfig({
routeRules: {
// Marketing pages — pre-renderizadas en build time (SSG)
'/': { prerender: true },
'/about': { prerender: true },
'/pricing': { prerender: true },
// Blog — ISR (Incremental Static Regeneration) cada hora
'/blog': { isr: 3600 },
'/blog/**': { isr: 3600 },
// Dashboard — siempre SSR (autenticado, personalizado)
'/dashboard/**': { ssr: true },
// Zona de app interna — solo cliente (sin SSR)
'/editor/**': { ssr: false },
// API con caché SWR
'/api/products': { swr: 600 },
}
})
6. Layouts y Componentes
Layouts
Los layouts envuelven tus páginas con estructura común (navbar, footer, sidebar).
<!-- app/layouts/default.vue -->
<template>
<div class="min-h-screen flex flex-col">
<!-- Navbar -->
<AppNavbar />
<!-- Contenido de la página -->
<main class="flex-1 container mx-auto px-4 py-8">
<slot />
</main>
<!-- Footer -->
<AppFooter />
</div>
</template>
<!-- app/layouts/admin.vue — Layout alternativo -->
<template>
<div class="flex h-screen">
<!-- Sidebar -->
<AdminSidebar class="w-64 flex-shrink-0" />
<!-- Contenido principal -->
<div class="flex-1 overflow-auto">
<AdminHeader />
<main class="p-6">
<slot />
</main>
</div>
</div>
</template>
<!-- app/pages/dashboard/index.vue — Usar layout admin -->
<script setup lang="ts">
definePageMeta({
layout: 'admin' // Usa app/layouts/admin.vue
})
</script>
Componentes — Auto-imports
Nuxt importa automáticamente todos los componentes en app/components/. No necesitas import.
app/components/
├── AppNavbar.vue → <AppNavbar />
├── AppFooter.vue → <AppFooter />
├── blog/
│ ├── BlogCard.vue → <BlogCard />
│ └── BlogList.vue → <BlogList />
└── ui/
├── UiButton.vue → <UiButton />
└── UiModal.vue → <UiModal />
Nuxt 4 mejora: Los nombres de componentes ahora son consistentes entre auto-imports, Vue DevTools, y <KeepAlive>. En Nuxt 3 había discrepancias — ahora <KeepAlive name="BlogCard"> funciona exactamente como esperas.
Componente ejemplo completo
<!-- app/components/blog/BlogCard.vue -->
<template>
<article class="rounded-lg border p-6 hover:shadow-lg transition-shadow">
<!-- Imagen con lazy loading -->
<NuxtImg
v-if="post.image"
:src="post.image"
:alt="post.title"
class="w-full h-48 object-cover rounded mb-4"
loading="lazy"
/>
<!-- Categoría -->
<span class="text-sm text-primary font-medium uppercase tracking-wide">
{{ post.category }}
</span>
<!-- Título -->
<h2 class="text-xl font-bold mt-2 mb-3">
<NuxtLink :to="`/blog/${post.slug}`" class="hover:text-primary">
{{ post.title }}
</NuxtLink>
</h2>
<!-- Excerpt -->
<p class="text-gray-600 text-sm line-clamp-3">
{{ post.excerpt }}
</p>
<!-- Footer del card -->
<div class="flex items-center justify-between mt-4">
<div class="flex items-center gap-2">
<img
:src="post.author.avatar"
:alt="post.author.name"
class="w-8 h-8 rounded-full"
>
<span class="text-sm text-gray-700">{{ post.author.name }}</span>
</div>
<time class="text-sm text-gray-500" :datetime="post.publishedAt">
{{ formatDate(post.publishedAt) }}
</time>
</div>
</article>
</template>
<script setup lang="ts">
// Definir props con TypeScript
interface Post {
slug: string
title: string
excerpt: string
image?: string
category: string
publishedAt: string
author: {
name: string
avatar: string
}
}
const props = defineProps<{
post: Post
}>()
// Función de utilidad
function formatDate(dateString: string): string {
return new Intl.DateTimeFormat('es-ES', {
day: 'numeric',
month: 'long',
year: 'numeric'
}).format(new Date(dateString))
}
</script>
Componentes con <KeepAlive> (Nuxt 4 mejorado)
<!-- app/app.vue -->
<template>
<NuxtLayout>
<!-- Mantiene el estado del componente al navegar entre páginas -->
<NuxtPage keepalive />
</NuxtLayout>
</template>
<!-- O selectivo por componente -->
<template>
<KeepAlive include="BlogList,ProductGrid">
<component :is="currentComponent" />
</KeepAlive>
</template>
7. Data Fetching: useFetch y useAsyncData
Este es uno de los cambios más importantes de Nuxt 4. El sistema de data fetching fue completamente revisado.
El problema que solucionó Nuxt 4
// ❌ Problema en Nuxt 3:
// Dos componentes usando la misma key creaban dos refs SEPARADAS
// → Datos duplicados, inconsistencia, bugs sutiles
// Componente A
const { data: user } = useAsyncData('user-profile', () => fetchUser())
// Componente B (mismo key!)
const { data: user } = useAsyncData('user-profile', () => fetchUser())
// ← Creaba un ref NUEVO, independiente del anterior
// ✅ Solución en Nuxt 4:
// Mismo key = MISMO reactive ref compartido
// No hay duplicación de requests ni inconsistencia
useFetch — Para requests HTTP simples
// app/pages/blog/index.vue
<script setup lang="ts">
// Fetching simple — Nuxt deduplica si se usa en SSR + cliente
const { data: posts, status, error, refresh } = await useFetch('/api/posts', {
// Query params reactivos
query: {
page: 1,
limit: 10,
category: 'tech'
},
// Transformar respuesta
transform: (data) => data.items,
// Caché
getCachedData(key, nuxtApp) {
// Usa caché si los datos tienen menos de 5 minutos
const cached = nuxtApp.payload.data[key]
if (!cached) return undefined
const expiry = 5 * 60 * 1000
if (Date.now() - cached._timestamp > expiry) return undefined
return cached
},
// Watch — refetch cuando cambia un valor
watch: [selectedCategory],
// Server-only (no re-fetch en cliente)
server: true,
lazy: false,
})
</script>
useAsyncData — Para lógica de fetching personalizada
<script setup lang="ts">
const route = useRoute()
// Fetch con lógica personalizada
const { data: post, status } = await useAsyncData(
// Key único — crucial para caché y deduplicación
`post-${route.params.slug}`,
// Fetcher — cualquier función async
async () => {
const post = await $fetch(`/api/posts/${route.params.slug}`)
// Puedes hacer transformaciones aquí
return {
...post,
readTime: calculateReadTime(post.content)
}
},
// Opciones
{
// En Nuxt 4: default es undefined (no null como en Nuxt 3)
default: () => null,
// Watch para refetch automático
watch: [() => route.params.slug],
// Nuxt 4: getCachedData recibe contexto de por qué se re-fetch
getCachedData(key, nuxtApp, ctx) {
// ctx.cause puede ser: 'initial' | 'refresh' | 'watch' | 'navigate'
if (ctx?.cause === 'refresh') return undefined // No caché en refresh manual
return nuxtApp.payload.data[key]
}
}
)
</script>
$fetch — Fetch manual (sin caché ni SSR automático)
<script setup lang="ts">
// Para mutations, no para data inicial de página
async function submitForm(formData: FormData) {
try {
const response = await $fetch('/api/posts', {
method: 'POST',
body: formData,
})
navigateTo(`/blog/${response.slug}`)
} catch (error) {
// Manejo de error
console.error(error)
}
}
</script>
Abort Control (Nuevo en Nuxt 4.2)
// Cancelar requests de data fetching
const { data, refresh } = useAsyncData('search', () =>
$fetch(`/api/search?q=${searchQuery.value}`)
)
// El AbortController se maneja automáticamente cuando:
// - El componente se desmonta
// - Se llama refresh() antes de que termine la request anterior
// - La key cambia (con watch)
// Control manual:
const controller = new AbortController()
const { data } = await useFetch('/api/large-dataset', {
signal: controller.signal
})
// Cancelar desde fuera
function cancelRequest() {
controller.abort()
}
Patrones de data fetching comunes
<!-- Patrón: Lista con paginación reactiva -->
<template>
<div>
<div v-if="status === 'pending'">Cargando...</div>
<div v-else-if="error">Error: {{ error.message }}</div>
<div v-else>
<ProductCard v-for="product in data?.items" :key="product.id" :product="product" />
<Pagination v-model="page" :total="data?.total" :per-page="perPage" />
</div>
</div>
</template>
<script setup lang="ts">
const page = ref(1)
const perPage = ref(12)
// Refetch automático cuando cambia la página
const { data, status, error } = await useAsyncData(
'products',
() => $fetch('/api/products', {
query: { page: page.value, perPage: perPage.value }
}),
{ watch: [page, perPage] }
)
</script>
8. Composables y Estado Global
Los composables son funciones reutilizables que encapsulan lógica de Vue (Composition API).
useState — Estado global simple
// app/composables/useTheme.ts
export const useTheme = () => {
// useState es como ref() pero compartido entre todos los componentes
// y persistido en SSR (hydration-safe)
const theme = useState<'light' | 'dark'>('theme', () => 'light')
function toggleTheme() {
theme.value = theme.value === 'light' ? 'dark' : 'light'
}
return { theme, toggleTheme }
}
<!-- Usar en cualquier componente — mismo estado compartido -->
<script setup lang="ts">
const { theme, toggleTheme } = useTheme()
</script>
<template>
<button @click="toggleTheme">
Tema actual: {{ theme }}
</button>
</template>
Composable con data fetching
// app/composables/useUser.ts
export const useUser = () => {
const { data: user, status, refresh } = useAsyncData(
'current-user',
() => $fetch('/api/auth/me'),
{ server: true }
)
const isLoggedIn = computed(() => !!user.value)
const isAdmin = computed(() => user.value?.role === 'admin')
async function logout() {
await $fetch('/api/auth/logout', { method: 'POST' })
user.value = null
navigateTo('/login')
}
return {
user,
isLoggedIn,
isAdmin,
status,
refresh,
logout,
}
}
Composable con persistencia en localStorage
// app/composables/useLocalStorage.ts
export function useLocalStorage<T>(key: string, defaultValue: T) {
const data = useState<T>(key, () => {
// Solo en cliente (window no existe en servidor)
if (import.meta.client) {
const stored = localStorage.getItem(key)
return stored ? JSON.parse(stored) : defaultValue
}
return defaultValue
})
// Watch para persistir cambios
watch(data, (newValue) => {
if (import.meta.client) {
localStorage.setItem(key, JSON.stringify(newValue))
}
}, { deep: true })
return data
}
// Uso:
const cart = useLocalStorage('cart', { items: [], total: 0 })
9. El Servidor: Nitro y API Routes
Nitro es el motor del servidor de Nuxt. Escribe API routes con TypeScript nativo, zero-config.
API Route básica
// server/api/hello.ts
export default defineEventHandler((event) => {
return {
message: 'Hello from Nuxt 4!',
timestamp: new Date().toISOString()
}
})
// → GET /api/hello
API Route con método HTTP específico
// server/api/posts/index.get.ts → GET /api/posts
// server/api/posts/index.post.ts → POST /api/posts
// server/api/posts/[id].put.ts → PUT /api/posts/:id
// server/api/posts/[id].delete.ts → DELETE /api/posts/:id
// server/api/posts/index.get.ts
export default defineEventHandler(async (event) => {
const query = getQuery(event)
const page = Number(query.page) || 1
const limit = Number(query.limit) || 10
// Acceder a config del servidor
const config = useRuntimeConfig(event)
// Ejemplo con base de datos (cualquier ORM/driver)
const posts = await db.post.findMany({
skip: (page - 1) * limit,
take: limit,
orderBy: { publishedAt: 'desc' }
})
return {
items: posts,
total: await db.post.count(),
page,
limit
}
})
// server/api/posts/index.post.ts
export default defineEventHandler(async (event) => {
// Validar que el usuario está autenticado
const session = await getUserSession(event)
if (!session?.user) {
throw createError({
statusCode: 401,
statusMessage: 'No autorizado'
})
}
// Leer y validar el body
const body = await readBody(event)
if (!body.title || !body.content) {
throw createError({
statusCode: 400,
statusMessage: 'El título y contenido son requeridos'
})
}
// Crear el post
const post = await db.post.create({
data: {
title: body.title,
content: body.content,
authorId: session.user.id,
slug: slugify(body.title),
}
})
// Respuesta con status 201
setResponseStatus(event, 201)
return post
})
Middleware del servidor
// server/middleware/auth.ts
// Se ejecuta en TODAS las requests
export default defineEventHandler(async (event) => {
const url = getRequestURL(event)
// Solo aplicar a rutas /api/admin/*
if (!url.pathname.startsWith('/api/admin')) return
// Verificar token
const token = getHeader(event, 'authorization')?.replace('Bearer ', '')
if (!token) {
throw createError({ statusCode: 401, statusMessage: 'Token requerido' })
}
try {
const user = await verifyJWT(token)
event.context.user = user // Disponible en todos los handlers posteriores
} catch {
throw createError({ statusCode: 401, statusMessage: 'Token inválido' })
}
})
Server Utils (auto-importadas)
// server/utils/db.ts
// Exportar singleton de base de datos
import { drizzle } from 'drizzle-orm/node-postgres'
import { Pool } from 'pg'
import * as schema from '../db/schema'
const pool = new Pool({ connectionString: process.env.DATABASE_URL })
export const db = drizzle(pool, { schema })
// Disponible en todos los archivos de server/ sin importar
// server/api/posts.get.ts puede usar db directamente
// server/utils/email.ts
export async function sendEmail(to: string, subject: string, html: string) {
// Implementación con Resend, Nodemailer, etc.
}
// server/utils/slugify.ts
export function slugify(text: string): string {
return text
.toLowerCase()
.replace(/[^\w\s-]/g, '')
.replace(/[\s_-]+/g, '-')
.replace(/^-+|-+$/g, '')
}
10. La Carpeta shared/
Una de las adiciones más útiles de Nuxt reciente. La carpeta shared/ contiene código que puede usarse tanto en el cliente (app/) como en el servidor (server/).
¿Para qué usar shared/?
shared/
├── types/ ← Tipos TypeScript compartidos
│ ├── index.ts
│ ├── user.ts
│ └── product.ts
└── utils/ ← Funciones puras sin side effects
├── format.ts
├── validation.ts
└── constants.ts
Tipos compartidos
// shared/types/user.ts
export interface User {
id: string
name: string
email: string
role: 'admin' | 'user' | 'moderator'
avatar?: string
createdAt: string
}
export interface UserProfile extends User {
bio?: string
website?: string
social: {
twitter?: string
github?: string
linkedin?: string
}
}
// shared/types/api.ts
export interface ApiResponse<T> {
data: T
message?: string
meta?: {
page: number
limit: number
total: number
totalPages: number
}
}
export interface ApiError {
statusCode: number
statusMessage: string
data?: unknown
}
Utilidades compartidas
// shared/utils/format.ts
// ✅ Funciones puras — sin acceso a APIs de browser ni Node
export function formatPrice(
amount: number,
currency: string = 'EUR',
locale: string = 'es-ES'
): string {
return new Intl.NumberFormat(locale, {
style: 'currency',
currency,
}).format(amount)
}
export function truncate(text: string, maxLength: number): string {
if (text.length <= maxLength) return text
return text.slice(0, maxLength - 3) + '...'
}
export function formatDate(
date: string | Date,
locale: string = 'es-ES'
): string {
return new Intl.DateTimeFormat(locale, {
day: 'numeric',
month: 'long',
year: 'numeric',
}).format(new Date(date))
}
// shared/utils/validation.ts
export function isValidEmail(email: string): boolean {
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)
}
export function isValidUrl(url: string): boolean {
try {
new URL(url)
return true
} catch {
return false
}
}
export function validatePassword(password: string): {
valid: boolean
errors: string[]
} {
const errors: string[] = []
if (password.length < 8) errors.push('Mínimo 8 caracteres')
if (!/[A-Z]/.test(password)) errors.push('Al menos una mayúscula')
if (!/[0-9]/.test(password)) errors.push('Al menos un número')
return { valid: errors.length === 0, errors }
}
// Usar en componentes (app/)
const { formatPrice, truncate } = useUtils() // Auto-importado desde shared/utils
// O directamente:
const price = formatPrice(29.99) // También auto-importado
// Usar en server (server/api/)
export default defineEventHandler(() => {
return { price: formatPrice(29.99) } // Mismo shared/utils
})
11. TypeScript en Nuxt 4
Nuxt 4 trae la mejor experiencia TypeScript de cualquier versión.
Multi-proyecto TypeScript
Nuxt 4 genera proyectos TypeScript separados para cada contexto:
.nuxt/
├── tsconfig.app.json ← Solo para app/
├── tsconfig.server.json ← Solo para server/
├── tsconfig.shared.json ← Solo para shared/
└── tsconfig.json ← Root (incluye todos)
Esto significa:
- Autocompletado correcto en cada contexto
- No puedes accidentalmente usar
windowen código de servidor - No puedes usar variables solo-servidor en componentes cliente
- Errores de tipo más precisos y útiles
tsconfig.json (Nuxt 4 lo genera automáticamente)
// tsconfig.json — Solo necesitas este archivo
{
"extends": "./.nuxt/tsconfig.json"
}
¡Eso es todo! Nuxt genera el resto.
Tipos automáticos de Nuxt
// Nuxt genera automáticamente tipos para:
// 1. Rutas de tu app (type-safe routing)
const router = useRouter()
router.push('/blog/mi-post') // ✅ Autocompletado de rutas
router.push('/ruta-que-no-existe') // ❌ Error de TypeScript
// 2. API routes
const { data } = await useFetch('/api/posts')
// data.value.items → TypeScript conoce la estructura
// 3. Componentes auto-importados
// Los componentes en app/components/ tienen tipos automáticos
// 4. Composables auto-importados
// Los composables en app/composables/ tienen tipos automáticos
TypeScript Plugin (Nuxt 4.2 — Experimental)
// nuxt.config.ts
export default defineNuxtConfig({
experimental: {
typescriptPlugin: true // Mejor autocomplete, más rápido
}
})
Con el plugin activo, el language server de TypeScript usa un plugin de Nuxt para proporcionar información de tipos directamente integrada en tu editor, sin requerir el paso de generación de tipos.
Tipado de composables
// app/composables/useCart.ts
interface CartItem {
id: string
name: string
price: number
quantity: number
image?: string
}
interface CartState {
items: CartItem[]
coupon?: string
}
export function useCart() {
const cart = useState<CartState>('cart', () => ({
items: [],
coupon: undefined
}))
const total = computed(() =>
cart.value.items.reduce((sum, item) => sum + item.price * item.quantity, 0)
)
const itemCount = computed(() =>
cart.value.items.reduce((sum, item) => sum + item.quantity, 0)
)
function addItem(item: Omit<CartItem, 'quantity'>) {
const existing = cart.value.items.find(i => i.id === item.id)
if (existing) {
existing.quantity++
} else {
cart.value.items.push({ ...item, quantity: 1 })
}
}
function removeItem(id: string) {
cart.value.items = cart.value.items.filter(i => i.id !== id)
}
function clearCart() {
cart.value = { items: [], coupon: undefined }
}
return {
cart: readonly(cart),
total,
itemCount,
addItem,
removeItem,
clearCart,
}
}
12. Plugins y Middleware
Plugins
Los plugins se ejecutan cuando la aplicación arranca, en servidor y/o cliente.
// app/plugins/analytics.client.ts
// El sufijo .client.ts → Solo se ejecuta en el navegador
export default defineNuxtPlugin(() => {
// Inicializar analytics
const router = useRouter()
router.afterEach((to) => {
// Track page view
if (typeof gtag !== 'undefined') {
gtag('event', 'page_view', {
page_path: to.fullPath
})
}
})
})
// app/plugins/api.ts
// Sin sufijo → Ejecuta en cliente Y servidor
export default defineNuxtPlugin(() => {
const config = useRuntimeConfig()
// Crear instancia de $fetch con configuración base
const api = $fetch.create({
baseURL: config.public.apiBase,
headers: {
'Content-Type': 'application/json',
},
onRequest({ options }) {
// Añadir token si existe
const token = useCookie('auth-token').value
if (token) {
options.headers = {
...options.headers,
Authorization: `Bearer ${token}`
}
}
},
onResponseError({ response }) {
if (response.status === 401) {
navigateTo('/login')
}
}
})
// Proveer el cliente a toda la app
return {
provide: {
api // Accesible como this.$api o useNuxtApp().$api
}
}
})
Middleware de navegación
// app/middleware/auth.ts
// Se ejecuta antes de cada navegación a rutas que lo requieran
export default defineNuxtRouteMiddleware((to, from) => {
const { isLoggedIn } = useUser()
if (!isLoggedIn.value) {
// Guardar destino original para redirect después de login
return navigateTo({
path: '/login',
query: { redirect: to.fullPath }
})
}
})
// app/middleware/admin.ts
export default defineNuxtRouteMiddleware(() => {
const { isAdmin, isLoggedIn } = useUser()
if (!isLoggedIn.value) return navigateTo('/login')
if (!isAdmin.value) {
throw createError({
statusCode: 403,
statusMessage: 'No tienes permiso para acceder a esta página'
})
}
})
<!-- Aplicar middleware a una página -->
<script setup lang="ts">
definePageMeta({
middleware: ['auth', 'admin'] // Múltiples middleware en orden
})
</script>
// Middleware global (se ejecuta en TODAS las rutas)
// Nombrar: app/middleware/global-analytics.global.ts
// El sufijo .global.ts lo hace global automáticamente
13. SEO y Head Management
Nuxt 4 usa Unhead v2 para gestión del <head>. Mejor rendimiento y API más limpia.
useSeoMeta — La forma recomendada
<script setup lang="ts">
// useSeoMeta — tipado completo, sin typos posibles
useSeoMeta({
// Básicos
title: 'Mi página increíble',
description: 'La mejor descripción del mundo para SEO',
robots: 'index, follow',
// Open Graph (Facebook, LinkedIn, WhatsApp)
ogTitle: 'Mi página increíble',
ogDescription: 'La mejor descripción del mundo para SEO',
ogImage: 'https://ejemplo.com/og-image.jpg',
ogImageWidth: 1200,
ogImageHeight: 630,
ogType: 'website',
ogUrl: 'https://ejemplo.com/mi-pagina',
ogSiteName: 'Mi Sitio Web',
// Twitter / X
twitterCard: 'summary_large_image',
twitterSite: '@mitwitter',
twitterCreator: '@mitwitter',
twitterTitle: 'Mi página increíble',
twitterDescription: 'La mejor descripción del mundo',
twitterImage: 'https://ejemplo.com/twitter-image.jpg',
})
</script>
SEO dinámico para páginas de contenido
<!-- app/pages/blog/[slug].vue -->
<script setup lang="ts">
const route = useRoute()
const { data: post } = await useFetch(`/api/posts/${route.params.slug}`)
// SEO dinámico basado en el post — reactivo
useSeoMeta({
title: () => `${post.value?.title} | Mi Blog`,
description: () => post.value?.excerpt,
ogTitle: () => post.value?.title,
ogDescription: () => post.value?.excerpt,
ogImage: () => post.value?.coverImage,
ogType: 'article',
// Article-specific Open Graph
articlePublishedTime: () => post.value?.publishedAt,
articleModifiedTime: () => post.value?.updatedAt,
articleAuthor: () => post.value?.author.name,
articleTag: () => post.value?.tags,
})
// JSON-LD Schema markup para Google
useSchemaOrg([
defineArticle({
headline: () => post.value?.title,
description: () => post.value?.excerpt,
image: () => post.value?.coverImage,
datePublished: () => post.value?.publishedAt,
dateModified: () => post.value?.updatedAt,
author: {
'@type': 'Person',
name: () => post.value?.author.name,
}
})
])
</script>
useHead — Control completo del
<script setup lang="ts">
useHead({
// Título con template
titleTemplate: (title) => title ? `${title} | Mi Empresa` : 'Mi Empresa',
title: 'Dashboard',
// Meta tags
meta: [
{ name: 'viewport', content: 'width=device-width, initial-scale=1' },
{ name: 'theme-color', content: '#4f46e5' },
],
// Links
link: [
{ rel: 'canonical', href: 'https://ejemplo.com/dashboard' },
{ rel: 'icon', type: 'image/svg+xml', href: '/favicon.svg' },
{ rel: 'preconnect', href: 'https://fonts.googleapis.com' },
],
// Scripts
script: [
{
type: 'application/ld+json',
innerHTML: JSON.stringify({
'@context': 'https://schema.org',
'@type': 'WebSite',
name: 'Mi Empresa',
url: 'https://ejemplo.com'
})
}
],
// HTML lang attribute
htmlAttrs: { lang: 'es' },
// Body attrs
bodyAttrs: { class: 'antialiased' },
})
</script>
14. Nuxt Modules Esenciales
El ecosistema de módulos de Nuxt es enorme. Estos son los más importantes para un proyecto en 2025.
@nuxt/ui — Componentes de UI
pnpm add @nuxt/ui
// nuxt.config.ts
export default defineNuxtConfig({
modules: ['@nuxt/ui']
})
Nuxt UI v4 es ahora completamente gratuito (unificó Nuxt UI y Nuxt UI Pro). Incluye 110+ componentes listos para producción con Tailwind CSS v4.
<template>
<div>
<UButton color="primary" size="lg" @click="doSomething">
Click me
</UButton>
<UModal v-model="isOpen">
<UCard>
<template #header>
<h3>Título del modal</h3>
</template>
<p>Contenido del modal</p>
</UCard>
</UModal>
<UTable :data="users" :columns="columns" virtualize />
<UForm :schema="schema" :state="formState" @submit="onSubmit">
<UFormField label="Nombre" name="name">
<UInput v-model="formState.name" />
</UFormField>
<UButton type="submit">Enviar</UButton>
</UForm>
</div>
</template>
@nuxt/content — CMS basado en archivos
pnpm add @nuxt/content
content/
├── blog/
│ ├── primer-post.md
│ └── segundo-post.md
└── docs/
└── introduccion.md
<!-- app/pages/blog/[slug].vue -->
<script setup lang="ts">
const { data: post } = await useAsyncData(
() => queryContent(`/blog/${route.params.slug}`).findOne()
)
</script>
<template>
<article>
<h1>{{ post.title }}</h1>
<ContentRenderer :value="post" />
</article>
</template>
@nuxt/image — Optimización de imágenes
pnpm add @nuxt/image
<template>
<!-- Optimización automática: WebP, lazy, responsive -->
<NuxtImg
src="/hero.jpg"
alt="Hero image"
width="1200"
height="630"
format="webp"
quality="85"
loading="lazy"
sizes="sm:100vw md:50vw lg:800px"
/>
<!-- Con CDN externo -->
<NuxtImg
src="https://images.unsplash.com/photo-xxx"
provider="unsplash"
width="800"
height="600"
/>
</template>
@nuxtjs/i18n — Internacionalización
pnpm add @nuxtjs/i18n
// nuxt.config.ts
export default defineNuxtConfig({
modules: ['@nuxtjs/i18n'],
i18n: {
locales: [
{ code: 'es', name: 'Español', file: 'es.json' },
{ code: 'en', name: 'English', file: 'en.json' },
],
defaultLocale: 'es',
langDir: 'locales/',
strategy: 'prefix_except_default',
}
})
// locales/es.json
{
"welcome": "Bienvenido, {name}",
"nav": {
"home": "Inicio",
"about": "Sobre nosotros"
}
}
<script setup lang="ts">
const { t, locale, locales, setLocale } = useI18n()
</script>
<template>
<p>{{ t('welcome', { name: 'Juan' }) }}</p>
<NuxtLinkLocale to="/about">{{ t('nav.about') }}</NuxtLinkLocale>
</template>
@pinia/nuxt — Estado global avanzado
pnpm add pinia @pinia/nuxt
// app/stores/useUserStore.ts
export const useUserStore = defineStore('user', () => {
const user = ref<User | null>(null)
const isLoading = ref(false)
const isLoggedIn = computed(() => !!user.value)
async function fetchUser() {
isLoading.value = true
try {
user.value = await $fetch('/api/auth/me')
} finally {
isLoading.value = false
}
}
async function logout() {
await $fetch('/api/auth/logout', { method: 'POST' })
user.value = null
navigateTo('/login')
}
return { user, isLoading, isLoggedIn, fetchUser, logout }
})
15. Nuxt UI v4: El Design System Oficial
Nuxt UI v4 (lanzado en septiembre 2025) es ahora completamente gratuito y es una de las mejores librerías de UI para Vue.
Configuración
// nuxt.config.ts
export default defineNuxtConfig({
modules: ['@nuxt/ui'],
// Opcional: tema personalizado
ui: {
// Colores del design system
primary: 'violet',
gray: 'slate',
}
})
/* app/assets/css/main.css */
@import "tailwindcss";
@import "@nuxt/ui";
Theming con app.config.ts
// app/app.config.ts
export default defineAppConfig({
ui: {
primary: 'violet',
gray: 'slate',
button: {
// Customizar tokens del componente Button
rounded: 'rounded-full',
},
card: {
rounded: 'rounded-2xl',
shadow: 'shadow-lg',
}
}
})
Componentes clave
<template>
<div class="p-8 space-y-6">
<!-- Buttons -->
<div class="flex gap-3">
<UButton color="primary">Primary</UButton>
<UButton color="gray" variant="outline">Outline</UButton>
<UButton color="red" variant="soft">Soft</UButton>
<UButton loading>Loading</UButton>
</div>
<!-- Input con validación -->
<UFormField label="Email" name="email" :error="errors.email">
<UInput
v-model="email"
type="email"
placeholder="tu@email.com"
icon="i-heroicons-envelope"
/>
</UFormField>
<!-- Select -->
<USelectMenu
v-model="selected"
:items="options"
placeholder="Seleccionar..."
searchable
/>
<!-- Table con virtualización para datasets grandes -->
<UTable
:data="users"
:columns="[
{ key: 'name', label: 'Nombre', sortable: true },
{ key: 'email', label: 'Email' },
{ key: 'role', label: 'Rol' },
]"
virtualize
/>
<!-- Notification -->
<UButton @click="$notify({ title: '¡Éxito!', description: 'Post guardado', color: 'green' })">
Mostrar notificación
</UButton>
</div>
</template>
16. Deployment y Estrategias de Rendering
Modos de rendering en Nuxt
┌─────────────────────────────────────────────────────────────────┐
│ ESTRATEGIAS DE RENDERING EN NUXT 4 │
│ │
│ SSR (Server Side Rendering) — Default │
│ └── HTML generado por request en servidor │
│ └── Bueno para: Contenido dinámico, autenticado │
│ │
│ SSG (Static Site Generation) — nuxt generate │
│ └── HTML pre-generado en build time │
│ └── Bueno para: Blogs, documentación, landing pages │
│ │
│ ISR (Incremental Static Regen) — routeRules: { isr: N } │
│ └── SSG con revalidación periódica │
│ └── Bueno para: Tiendas, noticias, contenido semi-dinámico │
│ │
│ SWR (Stale While Revalidate) — routeRules: { swr: N } │
│ └── Sirve caché, regenera en background │
│ └── Bueno para: APIs, datos que cambian frecuentemente │
│ │
│ Híbrido — routeRules por ruta │
│ └── Diferentes estrategias para diferentes rutas │
│ └── Bueno para: Apps complejas con requisitos mixtos │
└─────────────────────────────────────────────────────────────────┘
Build para producción
# SSR (Node.js server)
pnpm build
# → .output/server/index.mjs
# SSG (Static)
pnpm generate
# → .output/public/
# Preview local
pnpm preview
Deployment en Vercel
// nuxt.config.ts
export default defineNuxtConfig({
nitro: {
preset: 'vercel'
}
})
// vercel.json (opcional — Nuxt lo detecta automáticamente)
{
"framework": "nuxt"
}
Deployment en Cloudflare Pages
// nuxt.config.ts
export default defineNuxtConfig({
nitro: {
preset: 'cloudflare-pages'
}
})
# Build para Cloudflare
pnpm build
# Sube .output/public/ y .output/server/ a Cloudflare Pages
Deployment con Docker (Node.js)
# Dockerfile
FROM node:20-alpine AS base
# Dependencias
FROM base AS deps
WORKDIR /app
COPY package.json pnpm-lock.yaml ./
RUN npm install -g pnpm && pnpm install --frozen-lockfile
# Build
FROM base AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN npm install -g pnpm && pnpm build
# Runner
FROM base AS runner
WORKDIR /app
ENV NODE_ENV=production
ENV PORT=3000
COPY --from=builder /app/.output ./.output
EXPOSE 3000
CMD ["node", ".output/server/index.mjs"]
# docker-compose.yml
version: '3.8'
services:
web:
build: .
ports:
- "3000:3000"
environment:
- DATABASE_URL=postgresql://user:pass@db:5432/mydb
- NUXT_PUBLIC_API_BASE=https://api.ejemplo.com
depends_on:
- db
db:
image: postgres:16-alpine
environment:
- POSTGRES_USER=user
- POSTGRES_PASSWORD=pass
- POSTGRES_DB=mydb
volumes:
- postgres_data:/var/lib/postgresql/data
volumes:
postgres_data:
17. Migrar de Nuxt 3 a Nuxt 4
La migración está diseñada para ser lo más sencilla posible. La mayoría de proyectos Nuxt 3 migran en minutos.
Método 1: Migración automática con Codemod
# Actualizar Nuxt
npx nuxt upgrade --dedupe
# Ejecutar codemods automáticos (recomendado)
pnpm dlx codemod@latest nuxt/4/migration-recipe
El codemod automáticamente:
- Mueve archivos a la carpeta
app/ - Actualiza imports
- Migra APIs deprecadas
- Actualiza referencias de configuración
Método 2: Migración manual paso a paso
Paso 1: Actualizar
pnpm add nuxt@^4.0.0
Paso 2: Crear la carpeta app/ y mover archivos
mkdir app
# Mover directorios de app
mv assets app/
mv components app/
mv composables app/
mv layouts app/
mv middleware app/
mv pages app/
mv plugins app/
mv utils app/
# Mover archivos raíz de app
mv app.vue app/
mv app.config.ts app/
mv error.vue app/
Paso 3: Actualizar paths en nuxt.config.ts
// ❌ Nuxt 3 — paths desde la raíz
css: ['~/assets/css/main.css']
// ✅ Nuxt 4 — el mismo path funciona (~ sigue siendo la raíz del proyecto)
// No necesitas cambiar los paths de assets con ~
Paso 4: Revisar cambios de TypeScript
pnpm typecheck
# Pueden aparecer errores que antes estaban ocultos — son legítimos, corregirlos
Paso 5: Revisar valores null → undefined
// ❌ Nuxt 3 — data empieza como null
const { data } = useFetch('/api/users')
if (data.value === null) { /* cargando o error */ }
// ✅ Nuxt 4 — data empieza como undefined
const { data } = useFetch('/api/users')
if (data.value === undefined) { /* cargando o error */ }
// O mejor:
if (!data.value) { /* cargando o error */ }
Breaking changes resumidos
BREAKING CHANGES DE NUXT 4:
├── Carpeta app/ (estructura nueva, la vieja sigue funcionando)
├── data y error por defecto: undefined (antes null)
├── Estilos inline: solo componentes Vue (no CSS global)
├── Nuxt 2 compat eliminada de @nuxt/kit
├── Unhead v2 (elimina algunas APIs deprecadas de v1)
└── TypeScript multi-proyecto (puede revelar errores ocultos)
Habilitar gradualmente (para proyectos grandes)
// nuxt.config.ts — Puedes habilitar solo ciertos cambios
export default defineNuxtConfig({
future: {
compatibilityVersion: 4
},
// Revertir cambios específicos si necesitas tiempo
experimental: {
defaults: {
useAsyncData: {
value: 'null' // Volver a null si tienes mucho código dependiente
}
}
}
})
18. Buenas Prácticas y Patrones Avanzados
Estructura de componentes recomendada
<!-- El orden estándar dentro de un SFC -->
<script setup lang="ts">
// 1. Imports (solo si no son auto-importados)
import { SomeExternalLibrary } from 'external-lib'
// 2. Props & Emits
const props = defineProps<{
userId: string
showAvatar?: boolean
}>()
const emit = defineEmits<{
updated: [user: User]
deleted: [id: string]
}>()
// 3. Composables & Stores
const router = useRouter()
const { user, isLoggedIn } = useUser()
// 4. Reactive state local
const isEditing = ref(false)
const formData = reactive({ name: '', email: '' })
// 5. Computed
const fullName = computed(() => `${user.value?.firstName} ${user.value?.lastName}`)
// 6. Data fetching
const { data: profile } = await useFetch(`/api/users/${props.userId}`)
// 7. Functions
function startEditing() {
isEditing.value = true
formData.name = user.value?.name ?? ''
}
async function saveChanges() {
await $fetch(`/api/users/${props.userId}`, {
method: 'PUT',
body: formData
})
emit('updated', formData as User)
isEditing.value = false
}
// 8. Lifecycle hooks
onMounted(() => {
// Solo en cliente
})
// 9. Watchers
watch(() => props.userId, () => {
// Reaccionar a cambios del prop
})
</script>
<template>
<!-- Template aquí -->
</template>
<style scoped>
/* Styles aquí (scoped = sin contaminar otros componentes) */
</style>
Error handling robusto
// app/composables/useApi.ts
export function useApi() {
const toast = useToast() // De Nuxt UI
async function request<T>(
url: string,
options?: Parameters<typeof $fetch>[1]
): Promise<T | null> {
try {
return await $fetch<T>(url, options)
} catch (error: any) {
// Errores del servidor
if (error.statusCode === 401) {
navigateTo('/login')
return null
}
if (error.statusCode === 403) {
toast.add({
title: 'Sin permiso',
description: 'No tienes acceso a este recurso',
color: 'red'
})
return null
}
// Error genérico
toast.add({
title: 'Error',
description: error.statusMessage || 'Algo salió mal',
color: 'red'
})
return null
}
}
return { request }
}
Patrón Repository para data access
// app/repositories/PostRepository.ts
export const usePostRepository = () => {
const config = useRuntimeConfig()
const baseUrl = config.public.apiBase
return {
async findAll(params?: { page?: number; category?: string }) {
return $fetch(`${baseUrl}/posts`, { query: params })
},
async findBySlug(slug: string) {
return $fetch(`${baseUrl}/posts/${slug}`)
},
async create(data: Omit<Post, 'id' | 'createdAt'>) {
return $fetch(`${baseUrl}/posts`, { method: 'POST', body: data })
},
async update(id: string, data: Partial<Post>) {
return $fetch(`${baseUrl}/posts/${id}`, { method: 'PUT', body: data })
},
async delete(id: string) {
return $fetch(`${baseUrl}/posts/${id}`, { method: 'DELETE' })
},
}
}
Optimistic Updates
<script setup lang="ts">
const { data: todos, refresh } = await useFetch('/api/todos')
async function toggleTodo(todo: Todo) {
// 1. Actualizar optimistamente (UI instantánea)
todo.completed = !todo.completed
try {
// 2. Persistir en servidor
await $fetch(`/api/todos/${todo.id}`, {
method: 'PATCH',
body: { completed: todo.completed }
})
} catch {
// 3. Revertir si falla
todo.completed = !todo.completed
// Mostrar error al usuario
}
}
</script>
19. Nuxt DevTools
Nuxt DevTools es una herramienta integrada que transforma la experiencia de desarrollo.
Activar DevTools
// nuxt.config.ts
export default defineNuxtConfig({
devtools: {
enabled: true, // Activar (default en desarrollo)
timeline: {
enabled: true, // Timeline de composables
}
}
})
Accede en: http://localhost:3000/__nuxt_devtools__/
Funcionalidades principales
NUXT DEVTOOLS — PANELES DISPONIBLES:
📄 Pages → Ver todas las rutas, navegar entre ellas
🧩 Components → Árbol de componentes en la página actual
🔄 Imports → Auto-imports activos y su origen
🗺️ Routes → Todas las rutas definidas
🔌 Plugins → Plugins cargados
📦 Modules → Módulos activos con sus configuraciones
💾 Assets → Archivos en assets/ y public/
📡 Fetch → Requests realizadas (con payloads)
🍍 Pinia → Estado de stores (si usas Pinia)
📝 Payload → Data del servidor transmitida al cliente
🔵 TypeScript → Errores de tipo en tiempo real
⚡ Performance → Métricas de rendimiento
Error overlay (Nuxt 4.2)
El nuevo error overlay en desarrollo es arrastrable y minimizable — puedes moverlo a cualquier esquina de la pantalla. Tu posición persiste entre recargas.
20. Qué viene: Nuxt 5
Nuxt 5 está en desarrollo activo y se espera pronto (Q1/Q2 2026).
NUXT 5 — LO QUE VIENE:
⚡ Nitro v3 + h3 v2
└── Mejor rendimiento en servidor
└── Tipos más fuertes en API routes
└── fetch más estrictamente tipado
🔧 Vite Environment API
└── Mejor experiencia de desarrollo
└── HMR más rápido
└── Soporte mejorado para Edge
🌊 SSR Streaming
└── Enviar HTML mientras se genera
└── TTFB más rápido para páginas lentas
└── React Suspense-like para Vue
♿ Módulo de Accesibilidad oficial
└── Auditorías integradas
└── Warnings en desarrollo
🔒 Fetch tipado de extremo a extremo
└── Si defines un server route con un tipo...
└── ...useFetch lo conoce automáticamente
└── Sin necesidad de duplicar tipos
📦 Multi-app support
└── Múltiples apps Nuxt en un monorepo
└── Compartir estado entre apps
Para seguir las novedades en tiempo real: nuxt.com/blog
Checklist: Proyecto Nuxt 4 Listo para Producción
### ✅ Configuración Base
- [ ] nuxt.config.ts con compatibilityDate definido
- [ ] runtimeConfig configurado (nunca secrets en public)
- [ ] .env con todas las variables documentadas en .env.example
- [ ] TypeScript strict: true activado
- [ ] ESLint con @nuxt/eslint configurado
### ✅ Rendimiento
- [ ] Imágenes usando <NuxtImg> con lazy loading
- [ ] routeRules configurado según necesidades de cada ruta
- [ ] experimental.asyncDataHandler activado (Nuxt 4.2+)
- [ ] CSS crítico no bloqueante
### ✅ SEO
- [ ] useSeoMeta en todas las páginas
- [ ] Canonical URLs configuradas
- [ ] Open Graph images (1200x630px)
- [ ] Sitemap generado (@nuxtjs/sitemap)
- [ ] robots.txt configurado
### ✅ Seguridad
- [ ] Headers de seguridad (nuxt-security module)
- [ ] Inputs del servidor validados (zod, valibot)
- [ ] Auth middleware en rutas protegidas
- [ ] Rate limiting en API routes
### ✅ DX (Developer Experience)
- [ ] DevTools activado en desarrollo
- [ ] Prettier + ESLint configurados
- [ ] Husky + lint-staged para commits limpios
- [ ] Tests con Vitest + @nuxt/test-utils
- [ ] CI/CD con GitHub Actions
### ✅ Deployment
- [ ] Variables de entorno configuradas en plataforma
- [ ] Health check endpoint (/api/health)
- [ ] Error monitoring (Sentry, etc.)
- [ ] Analytics configurado
- [ ] Preview deployments para PRs
Recursos Oficiales
- Documentación: nuxt.com/docs
- Blog oficial: nuxt.com/blog
- Módulos: nuxt.com/modules
- Templates: nuxt.com/templates
- Discord: discord.nuxtlabs.com
- GitHub: github.com/nuxt/nuxt
- Nuxt UI: ui.nuxt.com
- NuxtHub (deploy + database en Cloudflare): hub.nuxt.com
Artículo basado en Nuxt 4.3.1 — noviembre 2025. Última revisión: febrero 2026.
Tags: nuxt4 vue fullstack javascript typescript ssr webdev