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.
The 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 , 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.
Nitro: the part nobody talks about enough
Nitro 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.
You write your server code once:
server/api/products/[id].get.ts
export default defineEventHandler(async (event) => {
const id = getRouterParam(event, "id");
const product = await useStorage("data").getItem(`product:${id}`);
if (!product) {
throw createError({ statusCode: 404, statusMessage: "Not found" });
}
return product;
});
Then you pick a deployment preset in nuxt.config.ts (or let Nitro auto-detect from the environment):
nuxt.config.ts
export default defineNuxtConfig({
nitro: {
preset: "cloudflare-pages",
},
});
The 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.
The 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.
There's also a feature called direct API calls worth knowing about. When you write:
const products = await $fetch("/api/products");
...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.
was the new project layout. Your application code now lives under app/, separated from the rest of the repo:
my-nuxt-app/
├─ app/
│ ├─ assets/
│ ├─ components/
│ ├─ composables/
│ ├─ layouts/
│ ├─ middleware/
│ ├─ pages/
│ ├─ plugins/
│ ├─ utils/
│ ├─ app.vue
│ └─ error.vue
├─ content/
├─ public/
├─ shared/
├─ server/
└─ nuxt.config.ts
This 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.
The 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.
If 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.
Auto-imports: the magic and the seam
The 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.
It feels like magic the first day and like a black box the third. The honest answer is somewhere in between. Nuxt's 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.
Server routes feel like Express, run anywhere
The server/ directory is where Nitro shines for backend-style work. File names map to routes:
server/
├─ api/
│ ├─ products/
│ │ ├─ index.get.ts → GET /api/products
│ │ ├─ index.post.ts → POST /api/products
│ │ └─ [id].get.ts → GET /api/products/:id
│ └─ health.ts → ANY /api/health
├─ routes/ (no /api prefix)
│ └─ webhooks/
│ └─ stripe.post.ts → POST /webhooks/stripe
└─ middleware/
└─ auth.ts runs on every request
The 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.
Inside 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.
The 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.
Modules: the part Next.js doesn't have
This 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.
A few that ship in basically every serious project:
- 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.
- 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.
.↗ Original-Artikel auf dev.to lesenVollständiger Original-ArtikelDen kompletten Beitrag mit allen Details direkt auf dev.to lesen.
SOCIAL SHARE CARD GENERATOR