[{"data":1,"prerenderedAt":732},["ShallowReactive",2],{"post-en-nuxt4-introduction":3},{"id":4,"title":5,"body":6,"date":718,"description":17,"excerpt":719,"extension":720,"meta":721,"navigation":451,"path":722,"readTime":355,"seo":723,"slug":724,"stem":725,"tags":726,"__hash__":731},"en_blog/en/blog/nuxt4-introduction.md","Nuxt 4: What’s New and What Actually Changes for Developers",{"type":7,"value":8,"toc":707},"minimark",[9,14,18,23,26,29,38,41,51,57,63,73,87,98,101,111,217,220,224,231,359,374,378,385,493,496,500,503,603,609,683,687,693,703],[10,11,13],"h1",{"id":12},"nuxt-4-what-actually-changes","Nuxt 4: What Actually Changes",[15,16,17],"p",{},"If you work with Vue.js and have not yet looked at Nuxt 4, now is a good time to do so. The release is not a ground-up rewrite, but its structural changes are substantial enough to warrant a thorough overview before embarking on a new project.",[19,20,22],"h2",{"id":21},"what-nuxt-is-in-brief","What Nuxt Is, in Brief",[15,24,25],{},"Nuxt is a meta-framework built on top of Vue.js. It handles routing, server-side rendering (SSR), static site generation (SSG), data fetching, and a great deal more — all out of the box. The premise is straightforward: you write Vue components, Nuxt takes care of the infrastructure around them.",[15,27,28],{},"Version 3 introduced Vue 3's Composition API, the Nitro server engine, and an architecture centred on automatic imports. Nuxt 4 refines each of these pillars and introduces a handful of structural decisions that have a meaningful impact on how projects are organised and maintained.",[19,30,32,33,37],{"id":31},"the-central-change-the-app-directory","The Central Change: the ",[34,35,36],"code",{},"app/"," Directory",[15,39,40],{},"In Nuxt 3, a typical project is laid out as follows:",[42,43,48],"pre",{"className":44,"code":46,"language":47},[45],"language-text","├── components/\n├── composables/\n├── layouts/\n├── middleware/\n├── pages/\n├── plugins/\n├── server/\n├── nuxt.config.ts\n","text",[34,49,46],{"__ignoreMap":50},"",[15,52,53,54,56],{},"In Nuxt 4, all application code is consolidated under a dedicated ",[34,55,36],{}," directory:",[42,58,61],{"className":59,"code":60,"language":47},[45],"├── app/\n│   ├── components/\n│   ├── composables/\n│   ├── layouts/\n│   ├── middleware/\n│   ├── pages/\n│   ├── plugins/\n│   └── app.vue\n├── server/\n├── public/\n└── nuxt.config.ts\n",[34,62,60],{"__ignoreMap":50},[15,64,65,66,68,69,72],{},"This is not a cosmetic change. The explicit separation between application code (",[34,67,36],{},") and server-side logic (",[34,70,71],{},"server/",") enforces a cleaner boundary between concerns — one that becomes increasingly valuable as a codebase scales or as more contributors join a project.",[74,75,76],"blockquote",{},[15,77,78,79,82,83,86],{},"This behaviour was already available in Nuxt 3.x via the ",[34,80,81],{},"future.compatibilityVersion: 4"," flag in ",[34,84,85],{},"nuxt.config.ts",". In Nuxt 4, it is the default.",[19,88,90,91,94,95],{"id":89},"data-fetching-useasyncdata-and-usefetch","Data Fetching: ",[34,92,93],{},"useAsyncData"," and ",[34,96,97],{},"useFetch",[15,99,100],{},"The data-fetching primitives themselves remain familiar, but Nuxt 4 tightens their behaviour around reactivity and cache key management.",[15,102,103,104,106,107,110],{},"In Nuxt 3, ",[34,105,93],{}," would occasionally fail to re-trigger when a reactive dependency changed — a subtle source of stale data that was difficult to diagnose. In Nuxt 4, internal ",[34,108,109],{},"watch"," handling is more consistent and predictable:",[42,112,116],{"className":113,"code":114,"language":115,"meta":50,"style":50},"language-ts shiki shiki-themes github-dark github-light","// Automatically reactive to changes in `route.params.id`\nconst { data } = await useAsyncData(`product-${route.params.id}`, () =>\n  $fetch(`/api/products/${route.params.id}`),\n)\n","ts",[34,117,118,127,185,211],{"__ignoreMap":50},[119,120,123],"span",{"class":121,"line":122},"line",1,[119,124,126],{"class":125},"sryI4","// Automatically reactive to changes in `route.params.id`\n",[119,128,130,134,138,142,145,148,151,155,158,162,165,168,171,173,176,179,182],{"class":121,"line":129},2,[119,131,133],{"class":132},"scx8i","const",[119,135,137],{"class":136},"sQ3_J"," { ",[119,139,141],{"class":140},"s0DvM","data",[119,143,144],{"class":136}," } ",[119,146,147],{"class":132},"=",[119,149,150],{"class":132}," await",[119,152,154],{"class":153},"s-Z4r"," useAsyncData",[119,156,157],{"class":136},"(",[119,159,161],{"class":160},"sg6BJ","`product-${",[119,163,164],{"class":136},"route",[119,166,167],{"class":160},".",[119,169,170],{"class":136},"params",[119,172,167],{"class":160},[119,174,175],{"class":136},"id",[119,177,178],{"class":160},"}`",[119,180,181],{"class":136},", () ",[119,183,184],{"class":132},"=>\n",[119,186,188,191,193,196,198,200,202,204,206,208],{"class":121,"line":187},3,[119,189,190],{"class":153},"  $fetch",[119,192,157],{"class":136},[119,194,195],{"class":160},"`/api/products/${",[119,197,164],{"class":136},[119,199,167],{"class":160},[119,201,170],{"class":136},[119,203,167],{"class":160},[119,205,175],{"class":136},[119,207,178],{"class":160},[119,209,210],{"class":136},"),\n",[119,212,214],{"class":121,"line":213},4,[119,215,216],{"class":136},")\n",[15,218,219],{},"The governing principle is straightforward: cache keys must be unique and must reflect every dynamic parameter they depend on. A static key paired with dynamic data will produce cache collisions — this was equally true in Nuxt 3, but Nuxt 4 surfaces the problem more clearly, which encourages better habits from the outset.",[19,221,223],{"id":222},"nitro-and-server-routes","Nitro and Server Routes",[15,225,226,227,230],{},"Nuxt 4 continues to ship Nitro as its server runtime. API routes are defined declaratively in ",[34,228,229],{},"server/api/",", with file naming encoding both the path and the HTTP method:",[42,232,234],{"className":113,"code":233,"language":115,"meta":50,"style":50},"// server/api/products/[id].get.ts\nexport default defineEventHandler(async (event) => {\n  const id = getRouterParam(event, \"id\")\n  const product = await db.products.findById(id)\n  if (!product) throw createError({ statusCode: 404 })\n  return product\n})\n",[34,235,236,241,273,295,315,344,353],{"__ignoreMap":50},[119,237,238],{"class":121,"line":122},[119,239,240],{"class":125},"// server/api/products/[id].get.ts\n",[119,242,243,246,249,252,254,257,260,264,267,270],{"class":121,"line":129},[119,244,245],{"class":132},"export",[119,247,248],{"class":132}," default",[119,250,251],{"class":153}," defineEventHandler",[119,253,157],{"class":136},[119,255,256],{"class":132},"async",[119,258,259],{"class":136}," (",[119,261,263],{"class":262},"sFbx2","event",[119,265,266],{"class":136},") ",[119,268,269],{"class":132},"=>",[119,271,272],{"class":136}," {\n",[119,274,275,278,281,284,287,290,293],{"class":121,"line":187},[119,276,277],{"class":132},"  const",[119,279,280],{"class":140}," id",[119,282,283],{"class":132}," =",[119,285,286],{"class":153}," getRouterParam",[119,288,289],{"class":136},"(event, ",[119,291,292],{"class":160},"\"id\"",[119,294,216],{"class":136},[119,296,297,299,302,304,306,309,312],{"class":121,"line":213},[119,298,277],{"class":132},[119,300,301],{"class":140}," product",[119,303,283],{"class":132},[119,305,150],{"class":132},[119,307,308],{"class":136}," db.products.",[119,310,311],{"class":153},"findById",[119,313,314],{"class":136},"(id)\n",[119,316,318,321,323,326,329,332,335,338,341],{"class":121,"line":317},5,[119,319,320],{"class":132},"  if",[119,322,259],{"class":136},[119,324,325],{"class":132},"!",[119,327,328],{"class":136},"product) ",[119,330,331],{"class":132},"throw",[119,333,334],{"class":153}," createError",[119,336,337],{"class":136},"({ statusCode: ",[119,339,340],{"class":140},"404",[119,342,343],{"class":136}," })\n",[119,345,347,350],{"class":121,"line":346},6,[119,348,349],{"class":132},"  return",[119,351,352],{"class":136}," product\n",[119,354,356],{"class":121,"line":355},7,[119,357,358],{"class":136},"})\n",[15,360,361,362,365,366,369,370,373],{},"Nitro compiles these handlers into a self-contained, portable bundle that can be deployed across a range of targets: Node.js, edge runtimes such as Cloudflare Workers or Vercel Edge Functions, or as a fully static build. In Nuxt 4, Nitro's increased maturity is tangible — type inference is sharper, and utility helpers such as ",[34,363,364],{},"getRouterParam",", ",[34,367,368],{},"readBody",", and ",[34,371,372],{},"getCookie"," behave more reliably across deployment targets.",[19,375,377],{"id":376},"auto-imported-composables","Auto-Imported Composables",[15,379,380,381,384],{},"Any composable placed in ",[34,382,383],{},"app/composables/"," is automatically available throughout the application without an explicit import statement:",[42,386,388],{"className":113,"code":387,"language":115,"meta":50,"style":50},"// app/composables/useApi.ts\nexport const useApi = () => {\n  const config = useRuntimeConfig()\n  return $fetch.create({ baseURL: config.public.apiBase })\n}\n\n// Available directly in any component or page:\nconst api = useApi()\nconst data = await api(\"/products\")\n",[34,389,390,395,414,429,442,447,453,458,472],{"__ignoreMap":50},[119,391,392],{"class":121,"line":122},[119,393,394],{"class":125},"// app/composables/useApi.ts\n",[119,396,397,399,402,405,407,410,412],{"class":121,"line":129},[119,398,245],{"class":132},[119,400,401],{"class":132}," const",[119,403,404],{"class":153}," useApi",[119,406,283],{"class":132},[119,408,409],{"class":136}," () ",[119,411,269],{"class":132},[119,413,272],{"class":136},[119,415,416,418,421,423,426],{"class":121,"line":187},[119,417,277],{"class":132},[119,419,420],{"class":140}," config",[119,422,283],{"class":132},[119,424,425],{"class":153}," useRuntimeConfig",[119,427,428],{"class":136},"()\n",[119,430,431,433,436,439],{"class":121,"line":213},[119,432,349],{"class":132},[119,434,435],{"class":136}," $fetch.",[119,437,438],{"class":153},"create",[119,440,441],{"class":136},"({ baseURL: config.public.apiBase })\n",[119,443,444],{"class":121,"line":317},[119,445,446],{"class":136},"}\n",[119,448,449],{"class":121,"line":346},[119,450,452],{"emptyLinePlaceholder":451},true,"\n",[119,454,455],{"class":121,"line":355},[119,456,457],{"class":125},"// Available directly in any component or page:\n",[119,459,461,463,466,468,470],{"class":121,"line":460},8,[119,462,133],{"class":132},[119,464,465],{"class":140}," api",[119,467,283],{"class":132},[119,469,404],{"class":153},[119,471,428],{"class":136},[119,473,475,477,480,482,484,486,488,491],{"class":121,"line":474},9,[119,476,133],{"class":132},[119,478,479],{"class":140}," data",[119,481,283],{"class":132},[119,483,150],{"class":132},[119,485,465],{"class":153},[119,487,157],{"class":136},[119,489,490],{"class":160},"\"/products\"",[119,492,216],{"class":136},[15,494,495],{},"This is a genuine productivity gain, though it comes with a caveat: on larger projects, liberal use of auto-imports can obscure where functionality originates, making code harder to navigate for developers unfamiliar with the codebase. A practical convention — one composable per file, named with precision — goes a long way toward mitigating this.",[19,497,499],{"id":498},"nuxt-content-v3","Nuxt Content v3",[15,501,502],{},"For projects that use Nuxt as a content platform — blogs, documentation sites, knowledge bases — Nuxt Content v3 is fully compatible with Nuxt 4 and represents a significant step forward. MDC parsing (Markdown enriched with inline Vue components) is noticeably faster, and the content query API is now fully typed:",[42,504,506],{"className":113,"code":505,"language":115,"meta":50,"style":50},"const { data } = await useAsyncData(\"articles\", () =>\n  queryCollection(\"blog\")\n    .where(\"published\", \"=\", true)\n    .order(\"date\", \"DESC\")\n    .all(),\n)\n",[34,507,508,533,545,570,589,599],{"__ignoreMap":50},[119,509,510,512,514,516,518,520,522,524,526,529,531],{"class":121,"line":122},[119,511,133],{"class":132},[119,513,137],{"class":136},[119,515,141],{"class":140},[119,517,144],{"class":136},[119,519,147],{"class":132},[119,521,150],{"class":132},[119,523,154],{"class":153},[119,525,157],{"class":136},[119,527,528],{"class":160},"\"articles\"",[119,530,181],{"class":136},[119,532,184],{"class":132},[119,534,535,538,540,543],{"class":121,"line":129},[119,536,537],{"class":153},"  queryCollection",[119,539,157],{"class":136},[119,541,542],{"class":160},"\"blog\"",[119,544,216],{"class":136},[119,546,547,550,553,555,558,560,563,565,568],{"class":121,"line":187},[119,548,549],{"class":136},"    .",[119,551,552],{"class":153},"where",[119,554,157],{"class":136},[119,556,557],{"class":160},"\"published\"",[119,559,365],{"class":136},[119,561,562],{"class":160},"\"=\"",[119,564,365],{"class":136},[119,566,567],{"class":140},"true",[119,569,216],{"class":136},[119,571,572,574,577,579,582,584,587],{"class":121,"line":213},[119,573,549],{"class":136},[119,575,576],{"class":153},"order",[119,578,157],{"class":136},[119,580,581],{"class":160},"\"date\"",[119,583,365],{"class":136},[119,585,586],{"class":160},"\"DESC\"",[119,588,216],{"class":136},[119,590,591,593,596],{"class":121,"line":317},[119,592,549],{"class":136},[119,594,595],{"class":153},"all",[119,597,598],{"class":136},"(),\n",[119,600,601],{"class":121,"line":346},[119,602,216],{"class":136},[15,604,605,606,608],{},"Configuration is handled in ",[34,607,85],{},":",[42,610,612],{"className":113,"code":611,"language":115,"meta":50,"style":50},"export default defineNuxtConfig({\n  modules: [\"@nuxt/content\"],\n  content: {\n    build: {\n      markdown: {\n        highlight: { theme: \"github-dark\" },\n      },\n    },\n  },\n})\n",[34,613,614,626,637,642,647,652,663,668,673,678],{"__ignoreMap":50},[119,615,616,618,620,623],{"class":121,"line":122},[119,617,245],{"class":132},[119,619,248],{"class":132},[119,621,622],{"class":153}," defineNuxtConfig",[119,624,625],{"class":136},"({\n",[119,627,628,631,634],{"class":121,"line":129},[119,629,630],{"class":136},"  modules: [",[119,632,633],{"class":160},"\"@nuxt/content\"",[119,635,636],{"class":136},"],\n",[119,638,639],{"class":121,"line":187},[119,640,641],{"class":136},"  content: {\n",[119,643,644],{"class":121,"line":213},[119,645,646],{"class":136},"    build: {\n",[119,648,649],{"class":121,"line":317},[119,650,651],{"class":136},"      markdown: {\n",[119,653,654,657,660],{"class":121,"line":346},[119,655,656],{"class":136},"        highlight: { theme: ",[119,658,659],{"class":160},"\"github-dark\"",[119,661,662],{"class":136}," },\n",[119,664,665],{"class":121,"line":355},[119,666,667],{"class":136},"      },\n",[119,669,670],{"class":121,"line":460},[119,671,672],{"class":136},"    },\n",[119,674,675],{"class":121,"line":474},[119,676,677],{"class":136},"  },\n",[119,679,681],{"class":121,"line":680},10,[119,682,358],{"class":136},[19,684,686],{"id":685},"key-takeaways","Key Takeaways",[15,688,689,690,692],{},"Nuxt 4 is best understood as a consolidation release: it sharpens what Nuxt 3 introduced rather than replacing it. The ",[34,691,36],{}," directory enforces better project organisation, reactivity in data fetching is more dependable, and Nitro has matured into a genuinely robust server runtime. For any new Vue.js project requiring SSR, it is the most defensible starting point available today.",[15,694,695,696,699,700,702],{},"Migration from Nuxt 3 is designed to be incremental. The ",[34,697,698],{},"compatibilityVersion: 4"," flag allows teams to adopt new behaviours selectively, without a disruptive big-bang migration. In practice — on my own portfolio project — the transition amounted to an afternoon of work, the bulk of which involved reorganising files into the ",[34,701,36],{}," directory and updating a handful of imports.",[704,705,706],"style",{},"html pre.shiki code .sryI4, html code.shiki .sryI4{--shiki-dark:#6A737D;--shiki-default:#6A737D}html pre.shiki code .scx8i, html code.shiki .scx8i{--shiki-dark:#F97583;--shiki-default:#D73A49}html pre.shiki code .sQ3_J, html code.shiki .sQ3_J{--shiki-dark:#E1E4E8;--shiki-default:#24292E}html pre.shiki code .s0DvM, html code.shiki .s0DvM{--shiki-dark:#79B8FF;--shiki-default:#005CC5}html pre.shiki code .s-Z4r, html code.shiki .s-Z4r{--shiki-dark:#B392F0;--shiki-default:#6F42C1}html pre.shiki code .sg6BJ, html code.shiki .sg6BJ{--shiki-dark:#9ECBFF;--shiki-default:#032F62}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html pre.shiki code .sFbx2, html code.shiki .sFbx2{--shiki-dark:#FFAB70;--shiki-default:#E36209}",{"title":50,"searchDepth":129,"depth":129,"links":708},[709,710,712,714,715,716,717],{"id":21,"depth":129,"text":22},{"id":31,"depth":129,"text":711},"The Central Change: the app/ Directory",{"id":89,"depth":129,"text":713},"Data Fetching: useAsyncData and useFetch",{"id":222,"depth":129,"text":223},{"id":376,"depth":129,"text":377},{"id":498,"depth":129,"text":499},{"id":685,"depth":129,"text":686},"2024-12-10",null,"md",{},"/en/blog/nuxt4-introduction",{"title":5,"description":17},"nuxt4-introduction","en/blog/nuxt4-introduction",[727,728,729,730],"Nuxt","Vue.js","Frontend","SSR","mI6rI4U7TeWsdZb2s83lb0AwKxK0rh38SJru2ItSaM8",1774645635810]