{
  "$type": "site.standard.document",
  "bskyPostRef": {
    "cid": "bafyreifuj44zbs5somvqgaxddi5jpgv2py4vuwkps2s7u2p2bu2lessggq",
    "uri": "at://did:plc:25rdn5elo5izoxrmtis34zuk/app.bsky.feed.post/3mq3t5pyorku2"
  },
  "coverImage": {
    "$type": "blob",
    "ref": {
      "$link": "bafkreihqywuhn45qg6bw6efdw6yodg5u73jz54uyfg56z27svh5mbnmuxa"
    },
    "mimeType": "image/webp",
    "size": 176106
  },
  "path": "/nazar-boyko/nuxt-in-2026-the-vue-full-stack-framework-that-deserves-more-attention-546k",
  "publishedAt": "2026-07-07T23:55:36.000Z",
  "site": "https://dev.to",
  "tags": [
    "vue",
    "nuxt",
    "typescript",
    "webdev",
    "July 2025",
    "the official docs",
    "headline change",
    "auto-import system",
    "hydration best-practices page",
    "Nuxt UI",
    "@nuxt/image",
    "@nuxtjs/i18n",
    "@nuxt/content",
    "@pinia/nuxt",
    "nazarboyko.com",
    "@click",
    "@nuxtjs"
  ],
  "textContent": "Everyone knows Next.js is the default full-stack JavaScript framework in 2026. The job ads say so, the AI tooling assumes so, the conference talks revolve around React Server Components, and even people who've never touched a Vue file have an opinion about which version of `getServerSideProps` they like best. Everyone is mostly right - and missing a piece of the story.\n\nThe piece they're missing is that on the other side of the framework wall, Nuxt has spent the last two years quietly turning into one of the most thoughtfully designed full-stack frameworks in the ecosystem. Nuxt 4 landed in July 2025, the project is now on v4.4.x, and Nuxt 5 with the new Nitro v3 engine is on the runway. If you last looked at Nuxt around the v2-to-v3 jump, when everything was breaking and the migration guide was longer than some books, you are looking at a different framework today.\n\nThis isn't an \"X killed Y\" piece - Next.js isn't going anywhere, and for plenty of teams it's the right call. But if you're picking a stack for a new project, or you write Vue and feel mildly defensive every time the LinkedIn algorithm shows you another React thread, it's worth understanding what Nuxt actually is in 2026 and why people who use it tend to stick with it.\n\n##  The thesis: convention plus Nitro\n\nTwo ideas hold the whole framework together.\n\nThe first is **convention over configuration** , in the original Rails sense. You don't wire up a router, you put files in `app/pages/`. You don't import components, you drop them in `app/components/` and reference them by name. You don't import `ref` or `computed` or your own composables, they're auto-imported with types intact. The framework has a strong opinion about where things go, and once your hands learn it, you stop thinking about plumbing entirely.\n\nThe second is **Nitro** , the server engine Nuxt sits on top of. Nitro is a separate open-source project (also from the UnJS team) that turns your `server/api/`, `server/routes/`, and `server/middleware/` files into a standalone Node-compatible server distribution. According to the official docs, the build output goes into `.output/` and is independent of `node_modules` - you can `scp` it onto a fresh VM with just Node installed and it runs. The same build can target AWS Lambda, Cloudflare Workers, Deno Deploy, Bun, Vercel Edge, Netlify, or a long-running Node process, with no code changes. That last sentence is doing a lot of work, so let's pull on it.\n\n##  Nitro: the part nobody talks about enough\n\nNitro is what makes Nuxt's deployment story unusual. Most JS frameworks have a \"preferred host\" - Next.js feels happiest on Vercel, Remix on Cloudflare, SvelteKit on whatever adapter you bolt on. Nuxt also has those friendly partners, but the underlying server engine is genuinely platform-agnostic by design.\n\nYou write your server code once:\n\n**`server/api/products/[id].get.ts`**\n\n\n\n    export default defineEventHandler(async (event) => {\n      const id = getRouterParam(event, \"id\");\n      const product = await useStorage(\"data\").getItem(`product:${id}`);\n\n      if (!product) {\n        throw createError({ statusCode: 404, statusMessage: \"Not found\" });\n      }\n\n      return product;\n    });\n\n\nThen you pick a deployment preset in `nuxt.config.ts` (or let Nitro auto-detect from the environment):\n\n**`nuxt.config.ts`**\n\n\n\n    export default defineNuxtConfig({\n      nitro: {\n        preset: \"cloudflare-pages\",\n      },\n    });\n\n\nThe same `defineEventHandler` runs on Cloudflare's edge runtime, on a Lambda cold start, on a Bun process, on a long-lived Node server. The storage layer (`useStorage()`) abstracts over filesystem, Redis, S3, Cloudflare KV, and a handful of other drivers, so you write `getItem(key)` and pick the backend in config.\n\nThe bit that surprises people coming from other frameworks is the **standalone output**. After `nuxt build`, your `.output/server/` directory contains a single bundled server with all dependencies inlined. No `npm install` on the production server. No `node_modules` to ship. The whole thing is designed to be trivially containerised, dropped into a serverless runtime, or copied somewhere static. Compare that to a typical Node app where you ship `package.json`, run `npm ci --omit=dev` on the box, and pray the lockfile matches - Nitro just hands you a self-contained directory.\n\nThere's also a feature called **direct API calls** worth knowing about. When you write:\n\n\n\n    const products = await $fetch(\"/api/products\");\n\n\n...the `$fetch` helper checks where it's running. On the browser, it does a normal HTTP call. On the server (during SSR), it skips the network entirely and calls the route handler function directly. No localhost loopback, no extra socket, no double serialisation. That's a free latency win that you'd have to engineer by hand in many other stacks.\n\n##  The Nuxt 4 happy path: app/ directory and friends\n\nNuxt 4's headline change was the new project layout. Your application code now lives under `app/`, separated from the rest of the repo:\n\n\n\n    my-nuxt-app/\n    ├─ app/\n    │  ├─ assets/\n    │  ├─ components/\n    │  ├─ composables/\n    │  ├─ layouts/\n    │  ├─ middleware/\n    │  ├─ pages/\n    │  ├─ plugins/\n    │  ├─ utils/\n    │  ├─ app.vue\n    │  └─ error.vue\n    ├─ content/\n    ├─ public/\n    ├─ shared/\n    ├─ server/\n    └─ nuxt.config.ts\n\n\nThis isn't a cosmetic rename. The split exists for two reasons. First, file watchers were doing a lot of unnecessary work watching `node_modules/` and `.git/` siblings - pulling app code into its own subdirectory makes dev-server startup measurably faster, especially on Windows and Linux. Second, the framework now generates **separate TypeScript projects** for app, server, shared, and config code. Your editor knows the difference between client and server context, autocompletes correctly in each, and stops offering you `window` in your `server/api/` handlers.\n\nThe `shared/` folder is the third star of the show. Types, validators, and pure utility functions that both the browser and the server need go there, and they're auto-imported on both sides. Before this existed, the typical Nuxt project had a `utils/` folder somewhere ambiguous, and people would accidentally pull a Node-only helper into client code, watch the bundle balloon, and curse for an hour. Now there's a designated place for \"this runs everywhere\" code, and the bundler enforces it.\n\nIf you're upgrading from Nuxt 3, the migration is gentle. Nuxt 4 keeps a compatibility mode for the v3 layout - the framework detects which directory structure you've adopted and rolls with it. Most teams can run `npx nuxt upgrade --dedupe` and a Codemod-powered migration script, and ship the same day.\n\n##  Auto-imports: the magic and the seam\n\nThe bit that polarises people is **auto-imports**. In Nuxt, you don't import `ref`, `computed`, `useFetch`, `definePageMeta`, `useRoute`, your own composables, or your own components. They appear in scope, fully typed, and the production bundle only includes what you actually used.\n\nIt feels like magic the first day and like a black box the third. The honest answer is somewhere in between. Nuxt's auto-import system is implemented with `unimport`, which generates type definitions into `.nuxt/imports.d.ts` and a small AST transform that rewrites bare references at build time. It is genuinely \"only what you use,\" not a global `window.ref` situation, and IDE autocompletion works as long as the Nuxt dev server (or `nuxt prepare`) has been run at least once.\n\nThere are a few gotchas worth knowing before you commit:\n\n  * **Nested directories aren't scanned by default.** Only `app/composables/` (and `index.ts` inside it) are picked up. If you put `app/composables/auth/useSession.ts` thinking organisation is free, the import silently won't resolve. You either flatten the folder or extend the scan paths in `nuxt.config.ts`.\n  * **The`use` prefix isn't decorative.** Composables that don't start with `use` won't be picked up by some linting rules, and Vue's own reactivity-tracking machinery uses that convention as a hint. Naming a composable `getUser` instead of `useUser` will work in some ways and break in others.\n  * **Auto-imports vs. Nuxt layers.** If you structure your project as multiple layers (the Nuxt feature for splitting a monorepo into composable mini-apps), the layer-override mechanism requires explicit imports for composables - auto-imports defeat the override priority. People hit this when they try to use layers and aren't sure why their override isn't winning.\n\n\n\nFor most projects, none of this is a problem. For larger codebases that lean hard on layers, the \"explicit imports for composables\" rule is something to plan around early.\n\n##  Data fetching: useFetch, useAsyncData, and the hydration trap\n\nThis is the part where Nuxt's design rewards you for following the path and punishes you for going off it. The framework's data-fetching story revolves around two composables: `useAsyncData` (low-level, you pass a function) and `useFetch` (higher-level, you pass a URL and it calls `$fetch` under the hood).\n\nThe reason they exist - and the reason you should use them instead of bare `$fetch` in a `<script setup>` - is **payload hydration**. When the page renders on the server, `useFetch` runs the request once, embeds the result in the rendered HTML payload, and when the client takes over it reads from the payload instead of re-fetching. One round trip total. With bare `$fetch`, you get two: one on the server during SSR, one on the client during hydration. Plus the inevitable hydration-mismatch warning when the two responses differ by a few milliseconds.\n\nHere's the correct version:\n\n**`pages/products.vue`**\n\n\n\n    <script setup lang=\"ts\">\n    const { data: products, error, pending, refresh } = await useFetch(\"/api/products\");\n    </script>\n\n    <template>\n      <div>\n        <p v-if=\"pending\">Loading…</p>\n        <p v-else-if=\"error\">Couldn't load products.</p>\n        <ul v-else>\n          <li v-for=\"product in products\" :key=\"product.id\">{{ product.name }}</li>\n        </ul>\n        <button @click=\"refresh\">Refresh</button>\n      </div>\n    </template>\n\n\nAnd the version that looks correct but isn't:\n\n**`pages/products-buggy.vue`**\n\n\n\n    <script setup lang=\"ts\">\n    // This compiles. It even mostly works. It will give you hydration warnings.\n    const products = ref([]);\n\n    onMounted(async () => {\n      products.value = await $fetch(\"/api/products\");\n    });\n    </script>\n\n\nThe second version skips SSR data fetching entirely. The page renders empty on the server, hydrates with an empty list, fetches on the client, then re-renders. Users see the flicker, search engines see no content, and you've thrown away half of what you're paying for by running a Nuxt app at all.\n\nThere are subtler traps. `useFetch` and `useAsyncData` must be called **synchronously at the top level** of `<script setup>` or another composable. Calling them inside an `async` function or after an `await` breaks the SSR-to-client handoff - the framework loses track of which call corresponds to which hydration slot. If you need conditional fetching, use the `immediate: false` option and trigger `refresh()` later, don't wrap the composable in an `if`.\n\nThe Nuxt docs have a whole hydration best-practices page dedicated to this, and it's worth reading once even if you think you understand SSR. The rules feel arbitrary until you internalise the SSR-payload model, then they feel obvious.\n\n##  Server routes feel like Express, run anywhere\n\nThe `server/` directory is where Nitro shines for backend-style work. File names map to routes:\n\n\n\n    server/\n    ├─ api/\n    │  ├─ products/\n    │  │  ├─ index.get.ts        → GET  /api/products\n    │  │  ├─ index.post.ts       → POST /api/products\n    │  │  └─ [id].get.ts         → GET  /api/products/:id\n    │  └─ health.ts              → ANY  /api/health\n    ├─ routes/                   (no /api prefix)\n    │  └─ webhooks/\n    │     └─ stripe.post.ts      → POST /webhooks/stripe\n    └─ middleware/\n       └─ auth.ts                runs on every request\n\n\nThe HTTP method goes in the filename suffix (`.get.ts`, `.post.ts`, `.delete.ts`, etc.). Catch-all routes use `[...path].ts`. Middleware in `server/middleware/` runs on every server request - useful for auth checks, logging, request ID injection.\n\nInside a handler, you have h3's helper toolkit: `getQuery`, `readBody`, `getRouterParam`, `getHeader`, `setCookie`, `sendRedirect`, `createError`. The body is parsed for you. Errors thrown as `createError({ statusCode, statusMessage })` are serialised as JSON. There's no router config to maintain, no middleware chain to compose by hand, no Express-style `app.use` boilerplate.\n\nThe result feels closer to a small Go web service than to a typical Node project. You drop a file, you have a route. You're shipping APIs, server-rendered pages, and the static asset pipeline from one project.\n\n##  Modules: the part Next.js doesn't have\n\nThis is where Nuxt's culture differs most from React-land. The official Nuxt modules registry curates **packages that integrate cleanly with the framework** : same configuration style, same lifecycle hooks, same auto-import contract. You install one, add it to `modules: [...]` in `nuxt.config.ts`, and it wires itself in.\n\nA few that ship in basically every serious project:\n\n  * Nuxt UI - 125+ accessible components built on Reka UI and Tailwind CSS. Includes 200,000+ icons via Iconify, dark/light mode out of the box, and i18n hooks for 50+ languages. It's the closest thing the Vue ecosystem has to shadcn/ui except more comprehensive and officially maintained.\n  * @nuxt/image - drop-in `<NuxtImg>` and `<NuxtPicture>` components that do responsive sizing, lazy loading, AVIF/WebP conversion, and integrate with 20+ image providers (Cloudinary, Imgix, Cloudflare Images, S3, etc.) through the same component API.\n  * @nuxtjs/i18n - locale-aware routing, lazy-loaded translation files, automatic SEO `hreflang` tags, browser language detection. Configures Vue I18n v11 under the hood.\n  * @nuxt/content - Markdown + Vue components as a CMS. Version 3 moved to an SQL-backed query layer for better performance on large content sets, kept the MDC syntax (Vue components inside Markdown), and added typed collections so your queries are typed end-to-end.\n  * @pinia/nuxt - Pinia is the de facto state management library for Vue, and the Nuxt integration handles SSR state serialisation for you.\n\n\n\nThe point isn't that React doesn't have equivalents - it has more options for almost all of these. The point is that the React equivalents are independent libraries you assemble yourself, each with its own configuration model, its own SSR story, its own opinion about file structure. Nuxt's curated module ecosystem trades some flexibility for the experience of \"I want auth, internationalisation, image optimisation, and a UI library, all working together\" being roughly twenty lines of `nuxt.config.ts`.\n\nFor solo developers and small teams, that's a real productivity multiplier. For larger teams who want to bring their own design system and their own state library, the modules are optional - you can run a bare Nuxt app and treat it as a Vue + Vite + Nitro stack with conventions.\n\n##  Where Nuxt loses to Next.js (and where it doesn't)\n\nIt would be dishonest to pretend the two frameworks are at parity. They aren't, and the reasons matter.\n\n**Where Next.js is ahead:**\n\n  * **Ecosystem size.** React's npm graph is bigger than Vue's, and that compounds. More third-party UI kits, more meeting-room \"everyone here knows React\" defaults, more chance that the library you need has React bindings first and Vue bindings six months later (or never).\n  * **AI tooling.** The Vercel AI SDK is React-first. Most LLM example code, most copy-paste integrations, and tools like v0 are built around React. Nuxt has caught up partially - there's been recent work on AI-friendly modules and Vercel's v0 has Vue support - but if \"drop in an OpenAI streaming chat component in an afternoon\" is your benchmark, Next.js is still the shorter path.\n  * **React Server Components.** Whether you love or hate RSCs, they're a genuine architectural innovation, and Vue/Nuxt doesn't have a direct equivalent yet. Nuxt has SSR, hybrid rendering, ISR-equivalent via on-demand prerendering, and Vue 3.6's Vapor mode for client-side perf, but there's no \"server component\" primitive in the same sense.\n  * **Job market.** This one's just demographics. There are more React jobs. Don't pick Nuxt because you think it'll be a career bet - pick it because it suits the project.\n\n\n\n**Where Nuxt is ahead, or at least different:**\n\n  * **Convention pressure.** Two senior Vue/Nuxt developers will write surprisingly similar apps. Two senior React/Next.js developers will pick different state libraries, different data-fetching primitives, different file organisations. Convention is a feature when you're onboarding a team or rotating people through a codebase.\n  * **Deployment portability.** Nitro's \"build once, deploy anywhere\" story is real and works. Next.js has steadily improved its non-Vercel story, but Nitro was platform-agnostic from day one.\n  * **Less churn.** Vue 3 has been stable since 2020. Nuxt 4 is a careful evolution of Nuxt 3, not a rewrite. React's lifecycle of \"everything you knew about data fetching is wrong now\" every 18 months has no Vue equivalent. For long-lived projects this is genuinely valuable.\n  * **Default i18n.** Multilingual sites are first-class in Nuxt via `@nuxtjs/i18n`. In Next.js they involve assembling a stack.\n  * **A genuinely small footprint.** A blank Nuxt app boots faster, ships less JavaScript, and uses less memory on the server than a blank Next.js app of equivalent functionality. The gap closes once you add features, but the starting point is leaner.\n\n\n\nThe right framework is the one whose tradeoffs match your project. If you're building a marketing site with a small CMS and a handful of dynamic routes, Nuxt is genuinely a delight. If you're building an enterprise dashboard that needs a particular React-only component library and a team of React-experienced engineers, the answer is obvious.\n\n##  What's coming: Nuxt 5 and Vue 3.6 Vapor mode\n\nTwo things are on the horizon that will shape what \"modern Nuxt\" means by the end of 2026.\n\n**Nuxt 5 and Nitro v3.** Per the Nuxt team's release post, Nuxt 5 will arrive \"on the sooner side\" and ships with Nitro v3 and h3 v2 under the hood. The headline improvements are around performance, the Vite Environment API for faster dev cycles, more strongly typed fetch calls, and built-in SSR streaming. None of these change the model - `useFetch` is still `useFetch`, your `server/api/` files still work - but the floor of \"how fast is a cold dev server\" and \"how typed is my response payload\" both move up.\n\n**Vue 3.6 Vapor mode.** Vue 3.6 introduces an opt-in compilation mode called Vapor that skips the virtual DOM entirely for components that opt in, generating direct DOM manipulation code instead. The Vue team has been clear that as of mid-2026 it's still beta and only covers a subset of template features (Composition API and `<script setup>` only - no Options API), but the performance numbers from the Vue Mastery and Vue School previews put Vapor in the same ballpark as Solid.js and Svelte 5 for raw rendering throughput. Once it's stable and Nuxt picks it up, you'll be able to mark a component `<script setup vapor>` and get the runtime savings without rewriting your logic.\n\nNeither of these is a reason to wait. Nuxt 4 is stable, production-ready, and being used by serious teams today. But they're worth knowing about if you're making a \"where will this stack be in 18 months\" decision - the curve is pointing in a healthy direction.\n\n##  A small honest moment\n\nIf you've read this far and you're still on Next.js: stay there. None of this is a reason to throw away a working stack. Framework migrations are a tax on your team that you pay in months and recoup over years, and the years have to be worth it.\n\nBut if you're choosing a stack for a new project and the React-first defaults aren't a strict requirement - if your team has Vue experience, if you value convention and a smaller surface area, if you want to deploy the same code to a VM and an edge worker without thinking - Nuxt is the framework that quietly deserves a longer look in 2026. The criticism that it's a \"Next.js alternative\" undersells it. It's a different design philosophy applied carefully over four major versions, and the result is one of the most coherent full-stack frameworks in the ecosystem.\n\nThe Vue community has spent the last few years building something good without making a lot of noise about it. The noise was never the point.\n\n_Originally published at nazarboyko.com._",
  "title": "Nuxt In 2026: The Vue Full-Stack Framework That Deserves More Attention"
}