Nuxt 4: la estructura app/ que rompe los tutoriales viejos
Nuxt 3 recibe parches —bug fixes y seguridad— hasta el 31 de julio de 2026. A partir de esa fecha, ninguno. Lo dice la roadmap oficial de Nuxt.
Si lees esto en julio de 2026, te quedan días. Si lo lees después, la fecha ya pasó y tu proyecto en Nuxt 3 corre sin soporte oficial.
Y aquí viene lo divertido: si hoy buscas "tutorial Nuxt 4" vas a encontrar decenas de artículos escritos en julio de 2025, la semana del lanzamiento. Te van a decir que crees una carpeta pages/ en la raíz del proyecto. Vas a hacerlo, no va a funcionar, y vas a perder cuarenta minutos pensando que te has equivocado en la instalación.
No te has equivocado. El tutorial está desfasado. Nuxt 4 movió esa carpeta de sitio y ese es exactamente el cambio del que casi nadie ha actualizado su contenido.
Este post está escrito contra Nuxt 4.5.0, la versión estable en julio de 2026. No contra la 4.0 del día del lanzamiento, que es lo que describe casi todo el material que vas a encontrar.
¿Qué es Nuxt 4?
Nuxt 4 es el meta-framework de Vue: coge Vue, que por sí solo es una librería de UI, y le añade routing por sistema de ficheros, renderizado en servidor, un servidor HTTP propio (Nitro), generación estática, auto-imports y un sistema de módulos. La versión 4 se publicó el 16 de julio de 2025 e introdujo el cambio más visible respecto a Nuxt 3: el código de aplicación se movió a la carpeta app/.
Si vienes de Angular o de React, la analogía más limpia es esta: Nuxt es a Vue lo que Next.js es a React, o lo que Analog aspira a ser en Angular. Un meta-framework — todo lo que necesitas para llevar una aplicación a producción, ya montado.
La tesis de Nuxt es que tú no deberías estar configurando nada de eso. Creas un fichero en el sitio correcto y el framework infiere la intención. Esto encanta o irrita, según de dónde vengas. Si llegas de Angular, donde todo es explícito y declarado, la cantidad de magia implícita te va a chirriar los primeros días. Es una decisión de diseño consciente, no un descuido.
Estructura de directorios en Nuxt 4: la carpeta app/
En Nuxt 4 el código de aplicación vive dentro de app/. Las carpetas pages/, components/, composables/, layouts/, middleware/, plugins/ y utils/, junto a app.vue, app.config.ts y error.vue, van en app/ — no en la raíz del proyecto como en Nuxt 3. Las carpetas de servidor y recursos (server/, public/, content/, layers/, modules/ y shared/) se quedan en la raíz. Lo tienes en la documentación oficial.
Si solo te llevas una cosa del post, que sea esta.
En Nuxt 3 todo eso vivía junto en la raíz: pages/, components/ y composables/ mezclados con server/, public/ y los ficheros de configuración. La raíz ahora queda para lo que no es aplicación Vue.
Así queda un proyecto:
mi-proyecto/
├── app/
│ ├── assets/
│ ├── components/
│ ├── composables/
│ ├── layouts/
│ ├── middleware/
│ ├── pages/
│ ├── plugins/
│ ├── utils/
│ ├── app.vue
│ ├── app.config.ts
│ └── error.vue
├── server/
├── public/
├── content/
├── layers/
├── modules/
├── shared/
├── nuxt.config.ts
├── package.json
└── tsconfig.json
Fíjate bien en la segunda mitad. server/, public/, content/, layers/, modules/ y shared/ no están dentro de app/. Se quedan en la raíz. Igual que node_modules/, .nuxt/ y .output/.
El fallo número uno al seguir material antiguo es crear pages/ en la raíz. En Nuxt 4 va en app/pages/. El framework no la encuentra donde tú la has puesto y no pasa nada visible: simplemente no tienes rutas y no entiendes por qué.
¿Por qué el cambio? Porque tener todo el código en la raíz obligaba a los file watchers a escanear .git/ y node_modules/, algo que retrasa el arranque de forma notable en sistemas que no son macOS. Y la separación entre lo que corre en el navegador y lo que corre en el servidor por fin es visible a simple vista, además de darte mejores autocompletados en el IDE.
Una nota sobre app/pages/: es opcional. Puedes construir una aplicación de una sola pantalla solo con app/app.vue y no crear la carpeta nunca. Pero en el momento en que quieras rutas y mantengas tu propio app.vue, necesitas poner <NuxtPage> dentro para que Nuxt tenga dónde renderizar la página actual. Si no lo pones, el enrutador funciona y no ves nada en pantalla.
Crear el proyecto y arrancarlo
Requisito previo: Node.js 22.x o superior. La documentación recomienda usar la LTS activa. Si tienes una 20 por ahí de otro proyecto, cámbiala antes de empezar o vas a pelearte con errores que no dicen lo que pasa.
Para crear el proyecto:
npm create nuxt@latest mi-proyecto
Con pnpm:
pnpm create nuxt@latest mi-proyecto
Con bun:
bun create nuxt@latest mi-proyecto
Y para arrancar el servidor de desarrollo abriendo el navegador automáticamente:
npm run dev -- -o
Con pnpm es pnpm dev -o y con bun bun run dev -o. Ojo al doble guion en la versión de npm: sin él, npm se come el flag y no lo pasa a Nuxt.
Lo que vas a usar todos los días
Rutas por ficheros
Cada fichero .vue dentro de app/pages/ genera una URL:
app/pages/
├── index.vue → /
├── about.vue → /about
└── posts/
└── [id].vue → /posts/:id
Los corchetes marcan segmentos dinámicos. Dentro del componente, lees el parámetro con useRoute():
<script setup lang="ts">
const route = useRoute()
console.log(route.params.id)
</script>
Para navegar entre páginas sin recargar, <NuxtLink>:
<template>
<NuxtLink to="/about">Sobre mí</NuxtLink>
<NuxtLink to="/posts/1">Primer post</NuxtLink>
</template>
Nada de esto se importa. Los auto-imports de Nuxt te dan useRoute, NuxtLink y todo lo que haya en app/components/ sin una sola línea de import. Al principio desorienta. A la semana no quieres volver atrás.
Data fetching: $fetch vs useFetch vs useAsyncData
Aquí hay tres herramientas y elegir mal es el error más común de quien llega nuevo.
| Herramienta | Cuándo usarla | Segura en SSR | Deduplica |
|---|---|---|---|
$fetch |
Acciones del usuario: click, envío de formulario | No | No |
useFetch |
Datos iniciales de un componente | Sí | Sí |
useAsyncData |
Lógica asíncrona que no es una simple llamada HTTP | Sí | Sí, por clave |
$fetch es la utilidad básica de red. No tiene protección para SSR ni deduplicación de peticiones. Úsala para interacciones del cliente disparadas por un evento: un click, el envío de un formulario.
useFetch es el envoltorio seguro para SSR. Hace la petición una sola vez en renderizado universal, en lugar de dispararla en servidor y otra vez en cliente al hidratar. Es lo que quieres para los datos iniciales de un componente:
<script setup lang="ts">
const { data, status, error, refresh } = await useFetch(39;/api/posts39;)
</script>
useAsyncData hace lo mismo pero con control más fino, y recibe una clave única como primer argumento para la caché. Es tu opción cuando la lógica asíncrona no es una simple llamada HTTP.
Ambos devuelven data, error, status, y funciones refresh, execute y clear. Y aceptan opciones que conviene conocer desde el día uno: lazy para no bloquear la navegación, server: false para pedir solo en cliente, pick para recortar el payload que viaja al navegador y watch para refrescar cuando cambie un valor reactivo.
Rutas de servidor
Esto es lo que a mí me terminó de convencer de Nuxt: el backend vive en el mismo proyecto y no es un añadido de segunda.
La carpeta server/ en la raíz se escanea sola:
server/
├── api/ # rutas prefijadas con /api
├── routes/ # rutas sin prefijo
├── middleware/ # corre antes de cada handler
├── plugins/ # hooks del ciclo de vida de Nitro
└── utils/ # helpers propios
Un fichero server/api/hello.ts responde en /api/hello. Un fichero en server/routes/ responde sin el prefijo. Todos los handlers usan defineEventHandler:
export default defineEventHandler((event) => {
return { hello: 39;world39; }
})
Para leer datos de la petición tienes tres utilidades que también son globales:
// server/api/hello/[name].ts
export default defineEventHandler((event) => {
const name = getRouterParam(event, 39;name39;)
return `Hola, ${name}`
})
// server/api/submit.post.ts
export default defineEventHandler(async (event) => {
const body = await readBody(event)
return { body }
})
getQuery(event) te da los parámetros de query. Y el sufijo del nombre del fichero define el método HTTP: test.get.ts atiende los GET, test.post.ts los POST, y cualquier otro método devuelve un 405.
Un aviso importante sobre esto. readBody te devuelve lo que venga, sin garantías de forma ni de tipo. En una ruta de servidor pública eso es una puerta abierta. Aquí es donde un schema de validación deja de ser buena práctica y pasa a ser obligatorio:
import { z } from 39;zod39;
const schema = z.object({
email: z.string().email(),
plan: z.enum([39;free39;, 39;pro39;]),
})
export default defineEventHandler(async (event) => {
const result = schema.safeParse(await readBody(event))
if (!result.success) {
throw createError({ statusCode: 400, statusMessage: 39;Payload inválido39; })
}
// result.data viene tipado, no hace falta castear nada
return { ok: true, email: result.data.email }
})
Validas el body antes de tocarlo y de paso obtienes el tipo inferido gratis. Si no tienes ese reflejo instalado, el curso de Zod cubre exactamente este patrón y se traslada tal cual a Nuxt.
Novedades de Nuxt 4.5
La 4.0 fue una versión de estabilidad. Nuxt 4.5 es donde está lo interesante.
Vite 8. Este es el titular. Arranques en frío más rápidos e internals movidos a Rolldown. Si tienes plugins de Vite propios, revisa la guía de migración antes de actualizar, porque algo se te va a mover. Sobre lo que trae Vite 8 en sí escribí un análisis aparte, y aplica entero aquí: cuando actualizas Nuxt, estás actualizando también tu bundler.
Rspack 2 sobre Rsbuild. El builder alternativo se moderniza. La configuración no cambia:
export default defineNuxtConfig({
builder: 39;rspack39;,
})
SSR streaming, experimental. Envía el HTML por partes en lugar de esperar a tenerlo todo. Mejora bastante el Time to First Byte:
export default defineNuxtConfig({
experimental: {
ssrStreaming: true,
},
})
Códigos de error estables. Los errores y avisos ahora vienen con un identificador fijo tipo NUXT_E1001, con explicación en línea y enlace a la documentación. Suena menor. No lo es: convierte "esto peta y no sé por qué" en una búsqueda con un solo resultado correcto.
useLayout. Un composable para leer el layout resuelto de la ruta actual:
const layout = useLayout()
Named views por convención de nombre. Múltiples salidas de renderizado usando el nombre del fichero:
app/pages/parent/child.vue
app/pages/parent/child@sidebar.vue
La opción enabled en useFetch y useAsyncData. Condiciona si la petición puede ejecutarse:
const query = ref(39;39;)
const { data } = await useFetch(39;/api/search39;, {
query: { q: query },
enabled: () => query.value.length > 2,
})
Sustituye la guarda manual dentro del watch por una barrera declarativa. Pero léete la letra pequeña antes de usarlo: mientras enabled es false se bloquea todo, incluidos execute, refresh y los disparos del watch. Y volver a true no relanza la petición por sí solo. Necesitas una fuente reactiva — la opción query del ejemplo — para que se dispare. Si pones solo enabled con una URL estática, la petición nunca sale y no hay ningún error que te lo diga.
Cuándo Nuxt tiene sentido y cuándo no
Voy a ser honesto, porque un post que solo vende no te sirve de nada.
Nuxt encaja cuando necesitas SEO real con contenido dinámico, cuando quieres frontend y backend en un mismo repositorio sin montar dos despliegues, y cuando el equipo ya conoce Vue. También encaja en productos que empiezan pequeños y aún no sabes si necesitarán servidor: Nitro te deja cambiar de destino de despliegue casi sin tocar código.
Nuxt no encaja si lo que tienes es una landing estática con tres secciones. Ahí estás pagando un runtime que no necesitas y Astro hace ese trabajo con menos JavaScript enviado al navegador. Tampoco encaja si tu equipo es de Angular y no hay ninguna intención de mantener Vue: el meta-framework no es el problema, el ecosistema paralelo sí.
Y hay un tercer caso que veo mucho: aplicaciones detrás de login, sin SEO, donde una SPA normal resuelve igual. Puedes usar Nuxt en modo cliente, claro. Pero si desactivas lo que lo hace especial, pregúntate qué estás comprando.
Sobre Nuxt 5: está en desarrollo e incluirá Nitro v3 más cambios adicionales. La roadmap oficial marcaba Q1 de 2026 como estimación, una fecha que ya quedó atrás. Lo relevante para ti no es cuándo sale, sino la política: Nuxt se compromete a soportar cada major un mínimo de seis meses tras la salida del siguiente. Traducido: cuando aparezca la 5, tu proyecto en 4 tiene medio año de margen garantizado. Esa previsibilidad vale más que cualquier feature.
Qué hacer hoy
Si mantienes un proyecto en Nuxt 3, el soporte oficial termina el 31 de julio de 2026. Pasada esa fecha no hay parches de seguridad. Empieza por lo aburrido: sube a Node 22, actualiza la dependencia y mueve tu código de aplicación dentro de app/. Ese movimiento de carpetas es el grueso de la migración, y no hace falta que lo hagas a mano — hay un codemod oficial:
npx codemod@latest nuxt/4/file-structure
Si no has tocado Nuxt nunca, crea un proyecto con el comando de arriba, mete un server/api/hello.ts, consúmelo con useFetch desde una página y observa la petición en la pestaña de red. Vas a ver que en la primera carga no hay ninguna llamada al API: el servidor ya trajo los datos. Ese momento explica Nuxt mejor que cualquier artículo.
Y si quieres construir algo real con esto sin pasar tres semanas dando vueltas a la arquitectura, es exactamente el proceso que trabajo en el curso de Construye con IA: de idea a producto con especificación primero y sin caos. En Dominicode Labs seguimos estos cambios de versión según salen, con los proyectos completos.
Preguntas frecuentes
¿Puedo seguir usando Nuxt 3 después del 31 de julio de 2026?
Tu aplicación va a seguir funcionando, no se apaga sola. Lo que termina es el soporte: Nuxt 3 recibe actualizaciones de mantenimiento con corrección de bugs y parches de seguridad hasta finales de julio de 2026. A partir de ahí, un fallo de seguridad en el framework se queda sin arreglar oficialmente. Para cualquier cosa en producción, esa es razón suficiente para planificar la migración.
¿Es obligatorio mover mi código a la carpeta app/?
No. La documentación oficial dice literalmente que la migración "no es obligatoria": Nuxt autodetecta la estructura antigua y sigue funcionando. Dicho eso, app/ es la convención oficial y lo que asumen el material y los módulos nuevos, así que retrasarlo solo aplaza el trabajo. El código de aplicación (assets, components, composables, layouts, middleware, pages, plugins, utils, más app.vue, app.config.ts y error.vue) va dentro de app/, mientras que server/, public/, content/, layers/, modules/ y shared/ se quedan en la raíz.
¿Cómo migro de Nuxt 3 a Nuxt 4?
Actualiza a Node.js 22, sube la dependencia de Nuxt y mueve el código de aplicación dentro de app/. Ese movimiento de carpetas es el grueso del trabajo y hay un codemod oficial que lo automatiza: npx codemod@latest nuxt/4/file-structure. La configuración (nuxt.config.ts) y la carpeta server/ se quedan donde están.
¿Puedo mantener la estructura de Nuxt 3 en Nuxt 4?
Sí. Nuxt 4 autodetecta la estructura antigua y funciona sin cambios. También puedes forzarla explícitamente con srcDir: '.' en nuxt.config.ts. Es una salida válida para ganar tiempo en un proyecto grande, no una decisión que quieras mantener a largo plazo.
¿Cuál es la diferencia entre useFetch y $fetch?
$fetch es la utilidad de red básica, sin protección para SSR ni deduplicación, pensada para peticiones disparadas por eventos del usuario. useFetch la envuelve y garantiza que en renderizado universal los datos se piden una sola vez, sin repetir la llamada al hidratar en el cliente. Regla simple: datos iniciales del componente con useFetch, acciones del usuario con $fetch.
¿Qué versión de Node.js necesito para Nuxt 4?
Node.js 22.x o superior, y la documentación recomienda usar la versión LTS activa. Si vienes de un entorno con Node 20, actualiza antes de crear el proyecto: los errores por versión antigua no siempre indican con claridad cuál es la causa real.
¿Necesito crear la carpeta app/pages/ siempre?
No, es opcional. Una aplicación de una sola pantalla puede vivir solo en app/app.vue. Ahora bien, si quieres routing por ficheros y mantienes tu propio app.vue, tienes que incluir el componente <NuxtPage> dentro para que Nuxt sepa dónde renderizar la página activa.
¿Cómo escribo un endpoint de API en Nuxt 4?
Creas un fichero dentro de server/api/ en la raíz del proyecto y exportas un defineEventHandler. Un server/api/hello.ts queda disponible en /api/hello. Para leer datos usas getRouterParam(event, 'nombre'), getQuery(event) y await readBody(event). El método HTTP se define con el sufijo del fichero, como submit.post.ts.
¿Nuxt 4 usa TypeScript por defecto?
Sí, los proyectos vienen configurados con TypeScript desde el inicio y Nuxt genera tipos automáticamente para rutas, componentes y las respuestas de tus endpoints de servidor. Si te interesa hacia dónde va el tipado en general, escribí sobre el nuevo compilador de TypeScript en Go y cómo cambia los tiempos de compilación.
Si prefieres ver esto en vídeo, en el canal de YouTube de Dominicode publico este tipo de análisis de versiones cada semana.
Por Bezael Pérez — Developer senior con más de 15 años de experiencia y fundador de Dominicode.
