What Nuxt 3 Actually Changes
Nuxt 3 isn't Vue 2 with a Nuxt wrapper bolted on — it's a ground-up rebuild that changes how you structure applications, how data flows between server and client, and how you think about performance. After integrating it into more than a dozen client projects, here are the features that have had the most impact on how we work day-to-day at MediaFront.
1. Auto-Imports: The Biggest Quality-of-Life Win
In Nuxt 3, Vue composables, your own components, and your own composables are all auto-imported. No import statements cluttering component files:
<script setup lang="ts">
// No imports needed — Nuxt resolves these automatically
const route = useRoute()
const { data: products } = await useAsyncData('products', () =>
$fetch<Product[]>('/api/products')
)
const selectedId = ref<string | null>(null)
const selected = computed(() => products.value?.find(p => p.id === selectedId.value))
</script>
This might seem cosmetic, but the knock-on effects are real: components are shorter, easier to scan, and onboarding new developers is measurably faster because there's no mental overhead from import path management.
Auto-imports extend to your composables/ directory too. Create composables/useCart.ts and useCart() is immediately available everywhere — no barrel files, no re-exports.
2. useAsyncData and useFetch: Data Fetching Done Right
Data fetching that works on both server and client without duplication is hard to get right. Nuxt 3 solves it with useAsyncData and useFetch:
// Server fetches this during SSR, serialises the result to the HTML payload,
// and the client hydrates from it — zero duplicate network requests
const { data: article, error } = await useAsyncData(
`article-${route.params.slug}`,
() => $fetch(`/api/articles/${route.params.slug}`),
{
transform: (raw) => ({
...raw,
// Computed on server once, not recalculated on client
readingTime: estimateReadingTime(raw.body),
publishedAt: new Date(raw.publishedAt).toLocaleDateString('nl-NL')
})
}
)
The key parameter (first argument) is critical: Nuxt uses it to deduplicate requests across navigations and to match server-rendered data with client hydration. Use a key that's unique to the data and includes any parameters that affect the result.
3. The Nitro Engine: One Server, Every Platform
Nitro is Nuxt 3's server engine. It compiles your server code once and can output to:
- Node.js (traditional hosting)
- Cloudflare Workers (edge)
- AWS Lambda / Azure Functions (serverless)
- Static HTML (CDN deployment)
Server API routes live in server/api/ and are auto-generated:
// server/api/products/[id].get.ts
export default defineEventHandler(async (event) => {
const id = getRouterParam(event, 'id')
if (!id) throw createError({ statusCode: 400, message: 'id is required' })
const product = await useDb().findProduct(id)
if (!product) throw createError({ statusCode: 404, message: 'Not found' })
// Built-in caching — cached at the Nitro layer, not the application layer
setHeader(event, 'Cache-Control', 'max-age=60, stale-while-revalidate=300')
return product
})
The file name encodes the HTTP method: .get.ts, .post.ts, .delete.ts. Nitro validates the method automatically.
4. Hybrid Rendering: Right Mode for Every Route
Nuxt 3 lets you set rendering strategy per route in nuxt.config.ts:
export default defineNuxtConfig({
routeRules: {
'/': { prerender: true }, // Static — fastest
'/blog': { prerender: true },
'/blog/**': { isr: 3600 }, // Regenerate hourly
'/products': { ssr: true, cache: true }, // SSR + edge cache
'/products/**': { ssr: true }, // SSR — SEO critical
'/account/**': { ssr: false }, // CSR — auth-gated
'/admin/**': { ssr: false },
}
})
In practice: marketing pages and blog posts are pre-rendered for instant delivery from CDN. Product pages use SSR so prices and stock are always fresh. The account dashboard skips SSR entirely — no benefit since it's behind auth and SEO doesn't apply.
5. TypeScript Integration: Fully Typed Without Configuration
Nuxt 3 generates types automatically for your routes, API endpoints, and content. After running nuxi prepare, you get:
// Auto-generated — navigateTo knows every valid route in your app
await navigateTo('/blog/nuxt3-features') // ✅ typed
await navigateTo('/blog/does-not-exist') // ❌ TypeScript error
// $fetch knows the response shape from server route return types
const product = await $fetch('/api/products/123')
// product is typed as Product — inferred from server/api/products/[id].get.ts
The auto-generated types file (.nuxt/types/) also includes typed useRoute().params based on your actual file structure in pages/. Rename a file, and TypeScript immediately flags every broken reference across the codebase.
Putting It Together: Real Performance Gains
These five features compound. On a recent MediaFront project — a B2B SaaS dashboard built with Nuxt 3:
- Time to First Byte: 180ms (SSR on Cloudflare Workers via Nitro)
- Lighthouse Performance: 97 mobile, 100 desktop
- Bundle size: 87kB initial JS (tree-shaking + code splitting, no manual config)
- Developer velocity: Features shipped 40% faster than the equivalent Nuxt 2 project
The improvements aren't magic — they're the result of a framework designed to make the right choice the default choice. Hybrid rendering defaults to SSR with static prerendering for eligible routes. Auto-imports eliminate a category of bugs entirely. Nitro's output adapters mean deployment strategy is a config change, not a rewrite.
Nuxt 3 is the most complete full-stack Vue framework available. If you're still on Nuxt 2, the migration is worth it.