Nuxt.js Interview Questions and Answers
Last updated:
Check out 40 of the most common Nuxt.js interview questions, then take an AI-powered practice interview
Q1What is Nuxt.js and how is it different from plain Vue?
BasicFundamentals
Answer
Nuxt.js is a meta-framework that wraps Vue 3 to give you everything you'd otherwise build yourself for a production app: file-based routing, server-side rendering, static site generation, a server runtime (Nitro), auto-imports for components and composables, a module ecosystem, and a typed configuration layer. Plain Vue is just the view layer, you'd need to wire up Vue Router, a build tool, an SSR bridge, an API server, and routing conventions on your own. The trade is convention over configuration: Nuxt makes 80% of decisions for you in exchange for being opinionated.
In 2026, the Nuxt 3 line (built on Vite + Nitro + Vue 3) is the stable choice; Nuxt 4 RC is shipping with lazy hydration and a slimmer bundle baseline. Concretely, the gap shows up the moment you run `npx nuxi@latest init`: you get `pages/`, `layouts/`, `server/`, `composables/`, `middleware/` and `plugins/` directories that Nuxt scans and compiles into router config, Nitro HTTP handlers and an auto-import manifest under `.nuxt/`. A `npm create vue@latest` app hands you `main.ts` and leaves the rest to you.
The second real difference is that Nuxt code is universal by default: the same component executes once in Node during SSR and again in the browser at hydration, which is why touching `window` or `localStorage` directly in `<script setup>` throws `window is not defined` in Nuxt but works fine in a Vite SPA. A senior interviewer usually follows up with 'when would you not use Nuxt', and the honest answer is a behind-login dashboard with no SEO requirement, where a plain Vue + Vite SPA gives you the same result with no Node process to operate.
# plain Vue: you wire routing, SSR and the API yourself
npm create vue@latest my-app
# Nuxt: conventions are the framework
npx nuxi@latest init my-app
# app.vue root component
# nuxt.config.ts typed config
# pages/ file-based routes
# server/api/ Nitro HTTP handlers
# composables/ auto-imported functions
# plugins/ runtime setup (.client / .server)
<!-- the same component runs in Node AND in the browser -->
<script setup>
const width = ref(0)
onMounted(() => { width.value = window.innerWidth }) // browser-only work
</script>
Key Points
- Vue 3 + Vite + Nitro server engine = Nuxt 3
- File-based routing, auto-imports, SSR/SSG out of the box
- Convention over configuration
- Nuxt 4 RC adds lazy hydration and smaller bundles
Q2How does file-based routing work in Nuxt 3?
BasicRouting
Answer
Any `.vue` file under `pages/` becomes a route. The filename maps to the URL path: `pages/index.vue` → `/`, `pages/about.vue` → `/about`, `pages/blog/[slug].vue` → `/blog/:slug` (dynamic), `pages/[...all].vue` → catch-all. Nested folders create nested routes, and `_` prefix marks private files Nuxt ignores.
You also need a `<NuxtPage />` component in your `app.vue` for the matched page to render. The route params show up via `useRoute().params.slug`. No manual router config, Nuxt scans the directory at build time and generates the router config for you.
Three conventions candidates routinely miss. First, `pages/[[slug]].vue` makes the parameter optional, so it matches both `/` and `/thing`. Second, nested routes need a parent file sitting next to the folder: `pages/settings.vue` must itself render a `<NuxtPage />` before `pages/settings/billing.vue` will appear inside it, otherwise you get a blank shell and no error.
Third, matching is specificity-ordered, static beats dynamic beats catch-all, so `pages/blog/new.vue` wins over `pages/blog/[slug].vue` for `/blog/new`. You can override the generated path with `definePageMeta({ path: '/p/:id' })` and reject junk params with `definePageMeta({ validate })`, which produces a real 404 instead of rendering a page with garbage in `route.params`. If the `pages/` directory does not exist at all, Nuxt skips vue-router entirely and ships a smaller client bundle. In Nuxt 4 the same directory moves to `app/pages/`.
<!-- pages/blog/[slug].vue -->
<script setup>
const route = useRoute()
const { data: post } = await useFetch(`/api/posts/${route.params.slug}`)
</script>
<template>
<article>
<h1>{{ post.title }}</h1>
<div v-html="post.body" />
</article>
</template>
Key Points
- File path = URL path
- [param] = dynamic, [...all] = catch-all
- <NuxtPage /> renders matched route
Q3What are pages, layouts, and components in Nuxt 3?
BasicStructure
Answer
Three directories with distinct roles. `pages/` defines routes (one file = one URL). `layouts/` defines the chrome around pages, header, footer, sidebar, and a page chooses one via `definePageMeta({ layout: 'admin' })`. `components/` holds reusable UI bits and they're auto-imported, so `<MyButton />` works anywhere without an import statement. Sub-folder structure becomes part of the component name: `components/forms/SignupForm.vue` → `<FormsSignupForm />`. There's also `components/global/` for components that should be available in markdown content or dynamic templates without explicit registration.
The `default.vue` layout is used unless a page opts into another. Three details senior interviewers probe. A layout must render `<slot />` and should have a single root element, because a fragment root breaks `<NuxtLayout>` transitions and Vue warns about non-element root nodes.
The layout name is resolved statically from the build, so `definePageMeta({ layout: someRef })` does not work; to swap layouts at runtime you call `setPageLayout('admin')` from middleware or setup, or set `definePageMeta({ layout: false })` and render `<NuxtLayout :name="layoutName">` yourself. Component auto-import names are derived from the path, so `components/forms/SignupForm.vue` becomes `<FormsSignupForm />` unless you set `components: [{ path: '~/components', pathPrefix: false }]` in `nuxt.config.ts`, which is the usual fix when a team hates the long names. Prefixing any usage with `Lazy` (`<LazyFormsSignupForm />`) moves that component into its own async chunk that only downloads when it actually renders.
<!-- layouts/admin.vue: single root element + a <slot /> -->
<template>
<div class="admin-shell">
<AdminSidebar />
<slot />
</div>
</template>
<!-- pages/reports.vue -->
<script setup>
definePageMeta({ layout: 'admin' })
</script>
// middleware/tenant.global.ts: pick the layout at runtime
export default defineNuxtRouteMiddleware((to) => {
if (to.path.startsWith('/admin')) setPageLayout('admin')
})
Q4What are composables in Nuxt 3 and how are they auto-imported?
BasicComposables
Answer
Composables are Vue 3 functions that encapsulate reactive state and logic, the Composition-API equivalent of React hooks. Any file in `composables/` is auto-imported, exposed by its filename (camelCased). So `composables/useCounter.ts` exporting a `useCounter()` function becomes globally available, no import needed.
Nuxt 3 also auto-imports its own built-ins: `useFetch`, `useAsyncData`, `useState`, `useRoute`, `useRouter`, `useRuntimeConfig`, `useNuxtApp`, and more. The auto-import is type-aware via the generated `.nuxt/types/imports.d.ts`, so TypeScript and your editor know what's in scope. Two things trip teams up in real projects.
Auto-import only scans the top level of `composables/` by default, so `composables/auth/useSession.ts` is invisible unless you re-export it from `composables/index.ts` or add `imports: { dirs: ['composables/**'] }` to `nuxt.config.ts`. And Nuxt composables must be called synchronously inside a setup context: calling `useState`, `useRuntimeConfig` or `useRoute` after an `await`, inside a `setTimeout`, or inside a plain click handler throws `[nuxt] A composable that requires access to the Nuxt instance was called outside of a plugin, Nuxt hook, Nuxt middleware, or Vue setup function`. The fix is to hoist the call to the top of setup and use the returned ref later, or wrap the async continuation in `nuxtApp.runWithContext(fn)`. If your editor reports `Cannot find name 'useFetch'`, the generated types are stale; run `nuxi prepare` to rebuild `.nuxt/`.
// composables/useCounter.ts
export const useCounter = () => {
const count = useState('counter', () => 0)
const increment = () => count.value++
return { count, increment }
}
// Any component, no import statement
<script setup>
const { count, increment } = useCounter()
</script>
Q5What is the `app.vue` file in Nuxt 3?
BasicStructure
Answer
`app.vue` is the root component for the entire app, it's the only file that's required at the top level. It typically renders `<NuxtLayout>` wrapping `<NuxtPage />`, which gives you the layout-around-page composition. If you don't have an `app.vue`, Nuxt provides a default one.
Putting `app.vue` in your project lets you control global wrappers like dark-mode containers, global error boundaries, or things that should render on every route. Keep it thin, most logic should live in layouts or pages. Behaviour worth knowing: if `app.vue` exists but never renders `<NuxtPage />`, routing is switched off completely and every URL renders identical markup, which is the classic 'my pages folder does nothing' bug.
Keeping `<NuxtLayout>` outside `<NuxtPage />` (the order below) means the layout instance survives navigation, so sidebar scroll position and open menus persist across route changes; nesting them the other way remounts the layout every time. `app.vue` also sits outside the page-level Suspense boundary, so a top-level `await useFetch()` there delays the first byte of HTML on every single route, keep slow fetches in the page. It is the natural home for `<NuxtLoadingIndicator />`, `<NuxtRouteAnnouncer />` (added in Nuxt 3.12 so screen readers announce client-side route changes), and any global toast or modal host. Nuxt 4 relocates the file to `app/app.vue` along with the rest of the client-side source.
<!-- app.vue -->
<template>
<NuxtLayout>
<NuxtPage />
</NuxtLayout>
</template>
Q6How do you fetch data in a Nuxt 3 page?
BasicData Fetching
Answer
Use `useFetch()` for typed, SSR-safe data fetching. It runs once on the server during SSR and the result is serialised into the HTML payload, so the client doesn't refetch on hydration. The return value is reactive: `{ data, pending, error, refresh }`.
For more control (custom keys, transforming the response), use `useAsyncData()` and pass `$fetch` inside. Avoid raw `fetch()` in setup, it bypasses Nuxt's SSR cache, so you double-fetch on hydration. `$fetch` (no `use`) is for client-side or event-handler calls. The mechanics matter when an interviewer digs in.
During SSR the resolved value is written into the `__NUXT_DATA__` script tag and rehydrated on the client, so anything unserialisable degrades: a `Date` comes back as an ISO string, class instances lose their prototype, use the `transform` option to normalise before it is serialised. `await useFetch()` in `<script setup>` suspends the component so the page waits for data; pass `{ lazy: true }` to render the shell immediately and drive the UI from `pending`, or `{ server: false }` to skip SSR for data that only matters after hydration. `watch: [filters]` re-runs the request when a dependency changes, and `{ immediate: false }` plus the returned `execute()` gives you manual control. The production failure mode to name out loud: cookies are not forwarded automatically when a server-side `useFetch` calls your own API, so the page renders logged-out during SSR and flips to logged-in after hydration. Fix it with `headers: useRequestHeaders(['cookie'])`.
<script setup>
const { data: products, pending, error, refresh } = await useFetch('/api/products', {
query: { limit: 20 },
})
</script>
<template>
<div v-if="pending">Loading…</div>
<div v-else-if="error">Error: {{ error.message }}</div>
<ProductCard v-else v-for="p in products" :key="p.id" :product="p" />
</template>
Q7How do you create a server route in Nuxt 3?
BasicServer Routes
Answer
Drop a file in `server/api/`. Nitro picks it up and exposes it at the matching URL. Each file exports a default handler wrapped in `defineEventHandler()`.
The event object gives you helpers: `getQuery(event)` for query params, `readBody(event)` for POST bodies, `getRouterParam(event, 'id')` for dynamic segments, and `setResponseStatus(event, 201)` for status codes. Dynamic routes use the same `[param]` convention: `server/api/users/[id].get.ts`. The `.get`/`.post`/`.put`/`.delete` suffix restricts the handler to that HTTP method.
A few details separate a junior answer from a senior one. Nitro treats two directories differently: `server/api/hello.ts` is served at `/api/hello`, while `server/routes/sitemap.xml.ts` is served at `/sitemap.xml` with no prefix, which is how you ship robots.txt, RSS feeds and payment webhooks that must live at a fixed path. Returning a plain object serialises to JSON automatically; return a string and you get `text/html` unless you call `setHeader(event, 'content-type', 'application/xml')` first.
Errors must be raised with `createError({ statusCode, statusMessage })`, because a bare `throw new Error('boom')` becomes an opaque 500 whose message Nitro strips in production. Do not trust `readBody` output: `readValidatedBody(event, schema.parse)` with Zod is the 2026 default. And remember these handlers run inside the same Nitro process as SSR, so one synchronous CPU-heavy loop (a big JSON parse, image resize, PDF build) blocks rendering for every other request on that instance.
// server/api/products/[id].get.ts
export default defineEventHandler(async (event) => {
const id = getRouterParam(event, 'id')
const product = await db.products.find(id)
if (!product) throw createError({ statusCode: 404, statusMessage: 'Not Found' })
return product
})
Q8What is `useState` in Nuxt 3 and how is it different from `ref`?
BasicState
Answer
`useState(key, factory)` is Nuxt's SSR-safe global state primitive. The key uniquely identifies the state across the app, so two components using the same key share the same reactive value. Crucially, it serialises across the SSR boundary, values set on the server are hydrated on the client, so you don't lose state on the client side.
Plain `ref()` works fine inside a single component but is per-instance: two components creating `const count = ref(0)` get independent counters. For cross-component shared state, you use `useState` (or Pinia for anything non-trivial). For private component state, you use `ref`/`reactive`.
Two implementation facts are worth having ready. `useState` stores its value in `nuxtApp.payload.state` under the key you pass, so the key must be stable and globally unique: one built from a loop index or a random number either collides or fails to hydrate. Only JSON-serialisable values cross the SSR boundary, so a `Map`, `Set` or class instance needs `definePayloadReducer`/`definePayloadReviver` registered in a payload plugin. On the server each request gets its own Nuxt instance, which is exactly why `useState` is request-scoped and safe; the dangerous pattern is a module-level `export const user = ref(null)`, created once per Node process and therefore shared by every concurrent visitor, which is how one user's session leaks into another user's page. A common follow-up is why `useState` needs a key at all: because the server render and the browser render happen in two different JavaScript runtimes, and the key is the only handle Nuxt has for matching the two copies of that value.
// SAFE: request-scoped and hydrates from the SSR payload
const user = useState('auth-user', () => null)
// LOCAL: fine, but every component instance gets its own copy
const isOpen = ref(false)
// DANGEROUS: module scope is shared across every SSR request
export const currentUser = ref(null) // leaks data between users
// non-JSON values need an explicit payload plugin
// plugins/payload.ts
export default definePayloadPlugin(() => {
definePayloadReducer('Map', (v) => v instanceof Map && [...v])
definePayloadReviver('Map', (v) => new Map(v))
})
Key Points
- `useState(key, factory)` = SSR-safe shared state
- `ref()` = local component state
- Same key = same value across components
- Hydrates from server to client automatically
Q9How do you handle SEO meta tags in Nuxt 3?
BasicSEO
Answer
Use `useHead()` or the higher-level `useSeoMeta()` composable in setup. They reactively update `<title>`, `<meta>`, Open Graph tags, JSON-LD, and link tags. Because Nuxt renders these on the server during SSR, crawlers see them in the initial HTML, critical for SEO. `useSeoMeta()` is the preferred API in 2026: it has typed props for every standard meta tag, including Twitter and Open Graph variants.
You can also set meta from `definePageMeta()` for static values that don't depend on fetched data. What a senior interviewer checks is whether you understand that the reactivity is real: `useSeoMeta` accepts getters, so `title: () => product.value.name` updates the tag when the fetch resolves, while passing `product.value.name` directly evaluates once and can ship an empty `<title>`. Site-wide title patterns belong in `app.head.titleTemplate: '%s | Brand'` inside `nuxt.config.ts`.
Canonicals and hreflang go through `useHead({ link: [{ rel: 'canonical', href }] })`, and JSON-LD through `useHead({ script: [{ type: 'application/ld+json', innerHTML: JSON.stringify(schema) }] })`. The failure mode to name out loud: on a route configured with `ssr: false`, or on anything wrapped in `<ClientOnly>`, none of these tags exist in the initial HTML, so the page looks perfect in DevTools and completely blank to a crawler. Verify with `curl -s https://site/page | grep -i '<title>'` rather than the Elements panel. For sitemaps, robots and OG image generation, the `@nuxtjs/seo` module set (which bundles `@nuxtjs/sitemap` and `@nuxtjs/robots`) beats hand-rolling `server/routes/sitemap.xml.ts`.
<script setup>
const { data: product } = await useFetch(`/api/products/${useRoute().params.id}`)
useSeoMeta({
title: () => product.value.name,
description: () => product.value.summary,
ogImage: () => product.value.image,
twitterCard: 'summary_large_image',
})
</script>
Q10What is `nuxt.config.ts` and what goes in it?
BasicConfiguration
Answer
`nuxt.config.ts` is the single source of project configuration: modules to load, build options, runtime config (env vars), routing rules (hybrid rendering), CSS files, app meta, Nitro options (preset, prerender list), and TypeScript settings. The `defineNuxtConfig({})` wrapper gives you full type-safety and autocomplete. Anything that needs to be controlled at build time goes here, runtime-only secrets should go in `runtimeConfig` (private side) so they're not bundled to the client.
The part interviewers actually test is `runtimeConfig`. Top-level keys are server-only and readable through `useRuntimeConfig()` in server routes and plugins; anything nested under `public` is inlined into the client bundle and readable by anyone with DevTools. Values are overridden at runtime by environment variables using the `NUXT_` prefix with SCREAMING_SNAKE mapping, so `runtimeConfig.apiSecret` is fed by `NUXT_API_SECRET` and `runtimeConfig.public.apiBase` by `NUXT_PUBLIC_API_BASE`.
The catch that burns teams in production: the key must already exist in the config with some default (an empty string is fine) or the environment variable is silently ignored, and the override happens when the server boots, which is what lets one Docker image be promoted from staging to production with different secrets. By contrast, a bare `process.env.X` read at module scope in `nuxt.config.ts` is baked in at build time. Other sections you touch constantly: `modules`, `css`, `routeRules` for hybrid rendering, `nitro` for the deploy preset and storage drivers, `vite` for raw bundler options, and `future.compatibilityVersion: 4` for opting into Nuxt 4 behaviour early.
// nuxt.config.ts
export default defineNuxtConfig({
modules: ['@nuxtjs/tailwindcss', '@pinia/nuxt', '@nuxt/image'],
runtimeConfig: {
apiSecret: '', // server-only
public: { apiBase: '/api' }, // exposed to client
},
routeRules: {
'/blog/**': { isr: 3600 }, // hybrid: ISR with 1h revalidate
'/admin/**': { ssr: false }, // SPA mode for admin
},
})
Q11How do you handle navigation programmatically in Nuxt 3?
BasicRouting
Answer
Use `navigateTo()`, Nuxt's universal navigation helper. It works on both server (sends a 302 redirect) and client (uses the router). For declarative links, use `<NuxtLink to="/path">` which gives you automatic prefetching when the link enters the viewport (huge perf win). `useRouter()` also exposes `.push()`, `.replace()`, `.go()`, `.back()` for traditional router API.
Always prefer `<NuxtLink>` for static links and `navigateTo()` for handler-driven navigation, they cooperate with Nuxt's middleware, route guards, and SSR redirects correctly. Details that come up in follow-ups. Inside middleware and setup you must `return navigateTo('/login')` rather than call it bare: returning it aborts the current render, while calling it without returning lets the server keep rendering the page it is also redirecting away from.
It takes options too, `{ replace: true }` to skip a history entry, `{ redirectCode: 301 }` because the SSR default is a 302, and `{ external: true }`, which is mandatory for off-site URLs or Nuxt throws `Navigating to an external URL is not allowed by default`. `<NuxtLink>` prefetches the target route's JavaScript chunk when the link enters the viewport; you can turn that off per link with `:prefetch="false"` or switch it to hover with the `prefetchOn="interaction"` prop added in Nuxt 3.13, which meaningfully cuts wasted bandwidth on long listing pages full of links. It also picks the right element automatically, an internal `<a>` with router handling, or an external `<a>` with `rel="noopener noreferrer"`.
<script setup>
const handleLogin = async (creds) => {
await $fetch('/api/login', { method: 'POST', body: creds })
await navigateTo('/dashboard')
}
</script>
<template>
<NuxtLink to="/about">About</NuxtLink>
</template>
Q12What is route middleware in Nuxt 3?
BasicMiddleware
Answer
Route middleware runs before navigation completes, perfect for auth guards, redirects, and feature flags. Define them in `middleware/` and reference them from a page via `definePageMeta({ middleware: 'auth' })`. Three flavours: anonymous (inline on the page), named (named file in `middleware/`), and global (`*.global.ts` in `middleware/`, runs on every route).
Inside a middleware, you have access to `to` (target route), `from` (current route), and can call `navigateTo()` to redirect or `abortNavigation()` to cancel. Runs on both server and client by default. Execution order and environment are what get probed.
Global middleware runs first, in alphabetical filename order, then named middleware in the order you list them in `definePageMeta({ middleware: ['auth', 'subscription'] })`. On a cold page load the chain runs on the server and then again in the browser during hydration; on client-side navigation it runs only in the browser. That double execution is why an auth guard reading `localStorage` works when you click around and crashes on refresh, guard it with `if (import.meta.server) return` or read the session from a cookie so both environments agree.
Returning `abortNavigation(createError({ statusCode: 403, statusMessage: 'Forbidden' }))` renders `error.vue` with a real status code instead of silently freezing on the current page. The security point worth stating explicitly: once the app is hydrated, route middleware is ordinary client-side JavaScript, so it controls what the UI shows and protects nothing. The enforcement has to live in `server/middleware/` or inside each `server/api/` handler.
// middleware/auth.ts
export default defineNuxtRouteMiddleware((to, from) => {
const user = useState('user')
if (!user.value) {
return navigateTo('/login?redirect=' + to.path)
}
})
// pages/dashboard.vue
<script setup>
definePageMeta({ middleware: 'auth' })
</script>
Q13What do `nuxi dev`, `nuxi build`, `nuxi generate`, and `nuxi preview` each produce?
BasicTooling
Answer
`nuxi dev` starts the Vite dev server with HMR plus an in-process Nitro server, and keeps regenerating the types under `.nuxt/` as you edit. `nuxi build` produces `.output/`, a self-contained server bundle you run with `node .output/server/index.mjs`, alongside `.output/public/` which holds the hashed client assets that belong behind a CDN with a one-year cache header. `nuxi generate` is the same build with `nitro.static: true`: it prerenders every discoverable route into `.output/public/` as plain HTML, so the site can sit on S3, Netlify or Cloudflare Pages with no Node process at all. `nuxi preview` runs the built output locally and is the only honest way to test route rules, ISR, caching and payload behaviour, because `nuxi dev` deliberately bypasses all of them, which is why cache bugs never reproduce in development. Two more commands come up constantly in CI. `nuxi prepare` regenerates `.nuxt/` types and is what you run before `tsc` in a pipeline, and it is also the fix when your editor claims `Cannot find name 'useFetch'`. `nuxi typecheck` runs vue-tsc over the project, which matters because a normal build does not type-check unless you set `typescript.typeCheck: true`. `nuxi analyze` opens a bundle treemap when you need to find the 400 KB chunk.
# develop: Vite HMR + in-process Nitro
npx nuxi dev --port 3000
# production server build -> .output/
npx nuxi build
node .output/server/index.mjs
# fully static build -> .output/public/*.html
npx nuxi generate
# run the REAL build locally: route rules, ISR and caching all active
npx nuxi preview
# CI hygiene
npx nuxi prepare # regenerate .nuxt types before type-checking
npx nuxi typecheck # vue-tsc over the whole project
npx nuxi analyze # bundle treemap
Key Points
- `nuxi build` -> .output/ server bundle; `nuxi generate` -> static HTML
- `nuxi preview` is the only place route rules and ISR actually run
- `nuxi prepare` fixes stale auto-import types
- Builds do not type-check unless `typescript.typeCheck` is on
Q14What are the rendering modes in Nuxt 3 and when do you use each?
IntermediateRendering
Answer
Nuxt 3 supports four modes, configurable per-route via `routeRules`. (1) **SSR (universal)**, default. Renders on every request, great SEO, dynamic per-user content. Use for product pages, dashboards, anything personalised. (2) **SSG (`nuxt generate`)**, renders all routes at build time to static HTML.
Cheapest to host (S3 + CDN), fastest TTFB. Use for marketing sites, docs, blogs where content doesn't change per user. (3) **SPA (`ssr: false`)**, client-side render only, no SSR HTML. Use for behind-auth admin panels where SEO doesn't matter. (4) **ISR (Incremental Static Regeneration)**, render once, cache for N seconds, regenerate in the background.
Use for blogs / product catalogs that change occasionally but get heavy traffic. The killer feature is **hybrid rendering**: mix all four in one app via `routeRules` in `nuxt.config.ts`. Two things to get right. `nuxi generate` is really `nuxi build` with `nitro.static: true`, so every route still has to be discoverable, either listed in `nitro.prerender.routes` or reachable by crawling links from `/`.
Pages that only exist behind a form POST or a JavaScript-driven filter are silently never generated and 404 in production. And `ssr: false` on a route rule does not remove the server, it ships an empty HTML shell plus the app bundle, which is fine for `/dashboard` and fatal for anything you want indexed. `swr` and `isr` differ mainly in who honours them: `swr` is Nitro's own stale-while-revalidate cache, while `isr` is additionally understood by Vercel and Netlify as a platform-level cache directive. Rules are matched by specificity, so pairing `'/blog/**': { isr: 3600 }` with `'/blog/preview': { ssr: true, headers: { 'cache-control': 'no-store' } }` behaves the way you would hope, the more specific pattern wins.
// nuxt.config.ts
export default defineNuxtConfig({
routeRules: {
'/': { prerender: true }, // SSG
'/blog/**': { isr: 3600 }, // ISR 1h
'/products/**': { swr: 600 }, // stale-while-revalidate 10m
'/dashboard/**': { ssr: false }, // SPA
'/api/**': { cors: true, headers: { 'cache-control': 's-maxage=60' } },
},
})
Key Points
- SSR for dynamic+SEO
- SSG for static content
- ISR for high-traffic semi-static
- Mix modes via routeRules
Q15What is the difference between `useFetch`, `useAsyncData`, and `$fetch`?
IntermediateData Fetching
Answer
All three end up calling the same underlying `$fetch` (ofetch), but the wrapper matters. `$fetch(url)` is the bare HTTP client, use it in event handlers, watchers, button clicks. It does NOT participate in SSR caching, so calling it in setup will fetch twice (once on server, once on hydration). `useFetch(url)` is a thin wrapper around `useAsyncData` + `$fetch`, convenient because the cache key is auto-derived from the URL + payload. `useAsyncData(key, fn)` is the most flexible, you provide an explicit key and any async function (database call via $fetch, multiple parallel fetches, transformation). Use it when `useFetch`'s URL-as-key isn't enough, or when you need to merge data from two sources.
Both `useFetch` and `useAsyncData` deduplicate concurrent calls with the same key, hydrate cleanly, and expose `{ data, pending, error, refresh, status }`. The follow-up most candidates fumble is what happens after hydration. `useFetch` does not re-run on hydration because the SSR payload covers it, but it does run fresh every time the user navigates into that page client-side, so a page you think of as cached hits the network again on each `<NuxtLink>` visit unless you supply `getCachedData`. Second, `useAsyncData` exposes `transform` and `pick`, and both run on the server before serialisation, so trimming a 200 KB API response to the five fields you render there actually shrinks the HTML payload rather than just the component's view of it. Third, during SSR a `$fetch('/api/x')` call to your own Nitro route is short-circuited in-process instead of making a real HTTP hop, which is fast but means the incoming request's cookies and headers are not carried over automatically; inside a server route use `event.$fetch` so context propagates.
// useFetch, simplest
const { data } = await useFetch('/api/products')
// useAsyncData, explicit key + custom logic
const { data } = await useAsyncData('hot-products', () => $fetch('/api/products', { query: { sort: 'hot' } }))
// $fetch, in handlers only
const onClick = async () => {
const result = await $fetch('/api/buy', { method: 'POST', body: { id: 1 } })
}
Q16What is hybrid rendering and how does ISR work in Nuxt 3?
IntermediateRendering
Answer
Hybrid rendering = mixing SSR, SSG, ISR, and SPA modes within one Nuxt app via `routeRules`. ISR (Incremental Static Regeneration) means a route is rendered once, the HTML is cached for `N` seconds (or indefinitely with `isr: true`), and served from cache without hitting the server logic. After the TTL expires, the next request triggers a background regeneration while still serving the stale copy, visitors never wait.
Behind the scenes, Nitro stores the cached responses in the configured cache driver (default: filesystem; production: Redis or CDN). The win: a Bewakoof-style product page can hit 10ms TTFB at 50k RPS while still updating prices when the catalog changes. ISR + on-demand revalidation (calling `await $fetch('/api/_nuxt/revalidate', { method: 'POST' })` after a CMS update) is the e-commerce sweet spot in 2026.
Interviewers dig into invalidation and correctness. The cache key for a route rule is the path, so query strings are not part of it by default and `/search?q=shoes` can serve the HTML generated for `/search?q=bags` unless you exclude that route or vary the key deliberately. Anything personalised must never sit behind ISR: render a logged-in user's name into a cached page and the next anonymous visitor gets it, which is the classic 'someone else's name in the navbar' incident. `nuxi dev` ignores ISR entirely, so these bugs only reproduce under `nuxi build && nuxi preview` or in staging. And on serverless targets the default filesystem cache driver is per-instance and ephemeral: a cold Lambda starts with an empty cache and ten warm instances hold ten independent copies of the page, which is exactly when you point `nitro.storage.cache` at Redis or let the platform's edge cache do the work instead.
// nuxt.config.ts
export default defineNuxtConfig({
routeRules: {
'/products/**': { isr: 3600 }, // regenerate every hour
'/blog/**': { isr: true }, // cache forever, revalidate on demand
'/preview/**': { ssr: true, headers: { 'cache-control': 'no-store' } },
},
nitro: {
storage: {
cache: { driver: 'redis', url: process.env.REDIS_URL },
},
},
})
Q17How do you manage state across components in Nuxt 3?
IntermediateState
Answer
Three approaches, in order of complexity. (1) `useState(key, factory)` for simple shared state, SSR-safe and lightweight. Best for things like a current user reference, a global modal flag, or a feature flag. (2) **Pinia** (`@pinia/nuxt`) for anything stateful or non-trivial, stores with state, getters, actions, plus devtools support, hot module reload, and TypeScript inference. Pinia replaced Vuex as the official Vue store in 2022 and is the standard in 2026. (3) Custom composables wrapping `useState` for module-shaped state that isn't big enough to warrant Pinia.
Avoid module-level singletons (`export const state = reactive({})`), they're shared across requests on the server in SSR mode, which leaks data between users. The Pinia specifics that come up: a store must be instantiated inside setup or a Nuxt context, because `useCartStore()` needs the active Pinia instance and calling it at module scope throws `getActivePinia() was called but there was no active Pinia`. State populated during SSR is serialised into the payload automatically by `@pinia/nuxt`, so you do not refetch on hydration, but the same JSON-only serialisation limits apply.
Destructuring kills reactivity: `const { total } = useCartStore()` hands you a frozen snapshot, so use `storeToRefs(store)` for state and getters while actions can be pulled off directly. For persistence, `pinia-plugin-persistedstate` writes to `localStorage`, which does not exist during SSR, so register it as a client-only plugin or accept a first-paint mismatch. The architectural point to land: cart and auth truth lives on the server, and the store is a cache of it, never the source.
// stores/cart.ts (Pinia)
import { defineStore } from 'pinia'
export const useCartStore = defineStore('cart', () => {
const items = ref<CartItem[]>([])
const total = computed(() => items.value.reduce((s, i) => s + i.price * i.qty, 0))
function add(item: CartItem) { items.value.push(item) }
return { items, total, add }
})
// any component
<script setup>
const cart = useCartStore()
cart.add({ id: 1, price: 999, qty: 1 })
</script>
Q18What causes hydration mismatches in Nuxt 3 and how do you fix them?
IntermediateHydration
Answer
A hydration mismatch happens when the HTML rendered on the server doesn't match what Vue tries to render on the client during hydration. Common causes: (1) Using `Date.now()` or `Math.random()` in a template, different value on server vs client. (2) Reading `window`, `document`, or `localStorage` in setup, these are undefined on the server, so the server renders one thing and the client another. (3) Conditional rendering based on browser-only state (viewport width, user agent). (4) Markup-level differences (server emits a `<p>` but client emits a `<div>` because of a prop check). Fixes: wrap browser-only code in `if (import.meta.client) {}`, use the `<ClientOnly>` component for whole subtrees that should only render on the client, or use `onMounted()` for any side-effect that reads the DOM.
Nuxt's dev console prints a detailed diff when a mismatch happens, read it carefully. Two sources people forget: invalid HTML that the browser silently repairs (a `<div>` nested inside a `<p>`, or `<tr>` elements without a `<tbody>`) is emitted verbatim by the server and re-parsed differently by the browser, and third-party scripts that mutate the DOM before Vue hydrates, chat widgets and A/B testing snippets being the usual culprits. In production the warning is stripped out, so the symptom becomes a component that renders once and then never updates, because Vue bailed out of hydration and replaced the whole subtree with a fresh client render, losing any event listeners you attached.
Reproduce it with `nuxi build && nuxi preview` and diff `curl -s <url>` against the hydrated DOM in DevTools, that comparison almost always points straight at the offending node. Note that `<ClientOnly>` takes a `#fallback` slot, so you can ship real placeholder markup at the right dimensions instead of causing a layout shift when the client content appears.
<!-- BAD, Date.now is different on server vs client -->
<p>{{ Date.now() }}</p>
<!-- GOOD, render only on client -->
<ClientOnly>
<p>{{ Date.now() }}</p>
</ClientOnly>
<!-- GOOD, use lifecycle for browser APIs -->
<script setup>
const width = ref(0)
onMounted(() => { width.value = window.innerWidth })
</script>
Q19How do you make code run only on the server or only on the client?
IntermediateSSR
Answer
Four mechanisms. (1) `import.meta.server` / `import.meta.client` runtime checks inside setup or composables. (2) File naming: `*.server.ts` for server-only modules (DB clients, secrets), `*.client.ts` for client-only (analytics, window APIs). Nuxt tree-shakes the wrong env out of the bundle. (3) The `<ClientOnly>` component wraps a template subtree so it renders ONLY on the client, useful for charts, maps, or anything that needs `window`. (4) `nuxt-only`-style plugins via `plugins/<name>.client.ts` or `plugins/<name>.server.ts`. Common interview gotcha: secrets must NEVER be in client-side code, put them under `runtimeConfig` (private side) and access via `useRuntimeConfig()` only on the server.
Precision matters in the follow-ups. `import.meta.server` and `import.meta.client` are replaced with literal `true`/`false` at build time, so the dead branch is tree-shaken and a heavy server-only import inside it never reaches the browser bundle. A runtime check like `typeof window === 'undefined'` gets no such treatment and can drag the entire dependency into your client chunk. `<ClientOnly>` still renders its children in the browser after mount, so it fixes hydration but does not remove any JavaScript; if you want zero JS for that subtree you need a `.server.vue` component or a server island. `process.server` and `process.client` are the Nuxt 2 spellings, still functional in Nuxt 3 but deprecated in favour of `import.meta.*` and unavailable on some edge runtimes. And the leak worth calling out: `runtimeConfig.public` is inlined into client JavaScript, so a token dropped there because 'the browser needs it' is published to everyone. Proxy that call through `server/api/` instead.
// plugins/sentry.client.ts, runs only in browser
export default defineNuxtPlugin(() => {
if (import.meta.client) {
initSentry({ dsn: useRuntimeConfig().public.sentryDsn })
}
})
// server/api/secret.ts, never bundled to client
export default defineEventHandler(() => {
const config = useRuntimeConfig() // includes private + public
return callExternalApi(config.apiSecret)
})
Q20How does `useFetch` caching work and when does it cause bugs?
IntermediateData Fetching
Answer
`useFetch` caches the response by an auto-derived key (URL + payload hash). On the server, the result is serialised into the HTML payload (`__NUXT_DATA__`); on hydration, the client reads from that payload instead of refetching. Subsequent calls to the same key within the same request return the cache.
Common bug: two `useFetch('/api/profile')` calls for two different users on the same page end up sharing a key, so the second user's data shows up under the first user's component. Fix: pass an explicit `key` option: `useFetch('/api/profile', { key: `profile-${userId}` })`. Other gotchas: the cache survives route navigation by default, set `getCachedData: () => undefined` to force refetch, or call `refresh()` to invalidate.
In 2026 the recommended pattern is to use Pinia or a small `useData()` composable when you need request-scoped cache control. Two more behaviours are worth naming. Since Nuxt 3.10 you can take full control with `getCachedData(key, nuxtApp)`: return `nuxtApp.payload.data[key]` to reuse the SSR payload and `undefined` to force a refetch, which is how you build 'cached on hydration, fresh on navigation'.
Those keys live in `nuxtApp.payload.data`, so you can inspect them in Nuxt DevTools and drop a single one with `clearNuxtData('profile-1')` after a mutation, which is cleaner than reaching for a `refresh()` handle from a component you no longer have. The production failure mode is subtler: because the payload is embedded in the served HTML, an ISR-cached or prerendered page freezes whatever `useFetch` returned at generation time, so a stale price or stock count survives long after the API is correct. Anything that must be current belongs in a client-side fetch or behind a short `swr` window.
// BAD, shared key across users
const { data: a } = await useFetch('/api/profile', { query: { id: 1 } })
const { data: b } = await useFetch('/api/profile', { query: { id: 2 } })
// a and b can end up identical due to key collision
// GOOD, explicit unique keys
const { data: a } = await useFetch('/api/profile', { query: { id: 1 }, key: 'profile-1' })
const { data: b } = await useFetch('/api/profile', { query: { id: 2 }, key: 'profile-2' })
Q21How do you implement authentication in a Nuxt 3 app?
IntermediateAuthentication
Answer
Three patterns in 2026. (1) **Cookie-based session**, server route sets an `HttpOnly` cookie on login (`setCookie(event, 'session', signed, { httpOnly: true, secure: true, sameSite: 'lax' })`), server middleware reads it on every request via `getCookie(event, 'session')`. Most secure against XSS. (2) **JWT in cookie**, same shape but the cookie contains a signed JWT; the server middleware verifies it. (3) **`nuxt-auth-utils`**, first-party module that wraps both patterns and gives you `useUserSession()` on the client and `requireUserSession(event)` on the server. For OAuth (Google, GitHub), `nuxt-auth-utils` ships built-in handlers, you just define a `server/api/auth/google.get.ts` and the module handles the flow.
Always pair with route middleware for client-side guards and server middleware for actual API protection, client-side checks alone are bypassable. What a senior interviewer pushes on is where the check actually runs and how the cookie is configured. `httpOnly` blocks XSS from reading the token, `secure` forces HTTPS, and `sameSite: 'lax'` stops most CSRF while still surviving the top-level redirect back from a payment gateway or an OAuth provider; setting `'strict'` there is the usual reason a Google login loop appears to succeed and then lands the user logged out. `nuxt-auth-utils` seals the session with `NUXT_SESSION_PASSWORD`, a 32-character secret that must be byte-identical on every instance, otherwise users are randomly signed out whenever the load balancer moves them to a different pod. The SSR wrinkle to raise unprompted: a server-side `useFetch` to your own API does not forward the browser's cookie automatically, so the first paint renders logged-out and flips after hydration. Pass `headers: useRequestHeaders(['cookie'])` to fix it.
// server/api/login.post.ts
export default defineEventHandler(async (event) => {
const { email, password } = await readBody(event)
const user = await verifyCredentials(email, password)
if (!user) throw createError({ statusCode: 401 })
await setUserSession(event, { user: { id: user.id, email: user.email } })
return { ok: true }
})
// middleware/auth.ts
export default defineNuxtRouteMiddleware(async () => {
const { loggedIn } = useUserSession()
if (!loggedIn.value) return navigateTo('/login')
})
Q22What is `nuxt/content` and when do you use it?
IntermediateContent
Answer
`@nuxt/content` is a file-based CMS module, write Markdown, YAML, JSON, or CSV files under `content/` and Nuxt parses them at build time into a queryable SQLite database (since v3). You then query the content via `queryCollection('blog').all()` or `.first()` with where/order/limit clauses. Renders MDC (Markdown Components), Markdown that can embed Vue components inline, perfect for marketing pages and docs.
Best fit: developer blogs, documentation sites, marketing pages where content updates ship via git commits. Not a fit for: user-generated content, anything that changes outside the build cycle. In India, lots of Nuxt-powered startup docs sites (Razorpay docs, Postman blog) use a similar pattern.
The version detail that matters in 2026: Content v3 dropped the old `queryContent()` chain in favour of collections declared in `content.config.ts` with `defineCollection` plus a Zod schema, queried through `queryCollection('blog')`. Frontmatter that violates the schema now fails the build instead of quietly rendering `undefined`, which alone justifies the upgrade on a docs site with many authors. Because the parsed content compiles to a SQLite database under `.data/content`, deployment changes shape: serverless targets bundle a WASM-backed database, and on Cloudflare you point the module at D1.
Two limits worth raising before the interviewer does. Content resolves at build time, so fixing a typo means a rebuild and redeploy unless the route sits behind ISR with on-demand revalidation. And full-text search over a few thousand documents needs a real index (Meilisearch, Algolia, or `@nuxtjs/algolia`) rather than pulling everything with `.all()` and filtering in the browser.
// content/blog/2026-01-launch.md
// ---
// title: Launch Day
// tags: [news]
// ---
// # Welcome to Launch Day
<script setup>
const { data: posts } = await useAsyncData('posts', () =>
queryCollection('blog').order('date', 'DESC').limit(10).all(),
)
</script>
<template>
<article v-for="p in posts" :key="p.path">
<NuxtLink :to="p.path">{{ p.title }}</NuxtLink>
</article>
</template>
Q23How do you handle errors in Nuxt 3?
IntermediateError Handling
Answer
Three layers. (1) **Per-fetch errors**, `useFetch` returns an `error` ref; check it and render an error UI in the template. (2) **Route-level errors**, for fatal page-load errors, the `error.vue` file at the project root catches them. It receives the error as a prop and decides what to render (404, 500, etc.). Call `clearError({ redirect: '/' })` to dismiss. (3) **Server route errors**, throw `createError({ statusCode: 404, statusMessage: 'Not found', data: { ... } })` from a server handler; Nuxt serialises it to the client with the right HTTP status.
Don't expose internal error messages, wrap database/3rd-party errors in your own ones with safe messages. For monitoring, hook `app:error` and `vue:error` in a plugin to ship errors to Sentry. Details worth volunteering. `error.vue` sits at the project root outside your layouts, so if you want site chrome on the 404 you must render `<NuxtLayout>` inside it yourself, which is why so many Nuxt error pages look unstyled. `showError()` triggers that same full-page state programmatically, and `createError({ fatal: true })` escalates a normally recoverable error into it.
Anything you attach under the `data` key of `createError` survives serialisation to the client and `statusMessage` is rendered, but `message`, `stack` and `cause` are stripped in production builds, which is exactly why a handler that throws a bare `Error` yields a blank 500 with no clue in the browser. For a real safety net you need three hooks, not one: `nuxtApp.hook('vue:error')` and `nuxtApp.hook('app:error')` in a client plugin, plus `nitroApp.hooks.hook('error')` in a Nitro plugin, because server-route failures never reach the Vue-side hooks at all.
// error.vue
<script setup>
const props = defineProps<{ error: { statusCode: number; statusMessage: string } }>()
</script>
<template>
<div>
<h1>{{ error.statusCode }}</h1>
<p>{{ error.statusMessage }}</p>
<button @click="clearError({ redirect: '/' })">Go home</button>
</div>
</template>
// server/api/users/[id].get.ts
export default defineEventHandler(async (event) => {
const user = await db.users.find(getRouterParam(event, 'id'))
if (!user) throw createError({ statusCode: 404, statusMessage: 'User not found' })
return user
})
Q24How do you write tests for a Nuxt 3 app?
IntermediateTesting
Answer
Use `@nuxt/test-utils` plus Vitest. Three test types. (1) **Unit tests** for composables and pure functions, Vitest alone, no Nuxt context needed. (2) **Component tests**, `mountSuspended(MyComponent)` from `@nuxt/test-utils/runtime` mounts a component with the full Nuxt runtime, so auto-imports and `useState` work. (3) **End-to-end tests**, `await setup({ server: true })` boots a real Nuxt server in the test, then use `$fetch(url)` or Playwright for browser tests. For mocking, `mockNuxtImport('useFetch', () => mockFn)` lets you stub auto-imported composables.
In CI, run unit + component tests first (fast), e2e last (slow). Many Indian shops also pair this with Playwright for full browser flows on the deployed staging URL. The setup detail that costs people an hour: `@nuxt/test-utils` needs the Nuxt environment enabled, via `defineVitestConfig({ test: { environment: 'nuxt' } })` from `@nuxt/test-utils/config`, and any spec that mounts a component has to run in it or the auto-imported composables are simply not defined. `mockNuxtImport` is hoisted the way `vi.mock` is, so it cannot reference a variable declared above it; the standard workaround is `vi.hoisted()`. `registerEndpoint('/api/products', () => [...])` stubs a Nitro route without touching component code, which is usually better than mocking `useFetch` because the real data path still executes.
End-to-end `setup({ server: true })` boots an actual production build, so budget 30 to 60 seconds of startup and keep those specs in a separate Vitest project so unit runs stay fast. Playwright against `nuxi preview` output is the only layer that catches hydration mismatches, since jsdom never hydrates real server HTML.
// tests/components/Button.test.ts
import { mountSuspended } from '@nuxt/test-utils/runtime'
import Button from '~/components/Button.vue'
it('renders a button with label', async () => {
const wrapper = await mountSuspended(Button, { props: { label: 'Click' } })
expect(wrapper.text()).toContain('Click')
})
// tests/e2e/home.test.ts
import { setup, $fetch } from '@nuxt/test-utils/e2e'
await setup({ server: true })
it('renders the home page', async () => {
const html = await $fetch('/')
expect(html).toContain('Welcome')
})
Q25What are Nuxt modules and how do you write one?
IntermediateModules
Answer
A Nuxt module is a function (wrapped by `defineNuxtModule()`) that extends Nuxt at build time, register components, add server routes, inject runtime config, set up middleware, push CSS files. The ecosystem is large: `@nuxt/image`, `@nuxt/content`, `@nuxtjs/tailwindcss`, `@pinia/nuxt`, `@vueuse/nuxt`, `nuxt-auth-utils`, `@sidebase/nuxt-auth`, dozens more. Writing your own is straightforward: a module exports `setup(options, nuxt)` that calls `addServerHandler`, `addComponent`, `addImports`, etc. Use cases for custom modules: encapsulating a multi-step setup (analytics + tracking + cookie banner) across multiple Nuxt projects, or wrapping an internal API client with auto-generated TypeScript types.
What separates a real answer is knowing that modules run inside the build process and therefore work through `@nuxt/kit` helpers rather than editing files: `addComponent`, `addImports`, `addServerHandler`, `addPlugin`, `extendPages` and `addTemplate` cover most cases, with `nuxt.hook('nitro:config', ...)` and `nuxt.hook('vite:extendConfig', ...)` for the rest. Any runtime code has to live in a separate `runtime/` folder resolved through `createResolver(import.meta.url)`, because the module file itself executes in Node at build time and is never shipped to the browser; putting a `ref()` in the module body is the classic beginner mistake. Order matters, modules run in the sequence listed in `nuxt.config.ts`, so one that reads `nuxt.options.runtimeConfig` must come after whatever populates it, and `installModule()` lets you depend on another module explicitly. For a shared internal module, `@nuxt/module-builder` produces the dual build and `.d.ts` files needed to publish it to a private registry.
// modules/analytics.ts
import { defineNuxtModule, addPlugin, createResolver } from '@nuxt/kit'
export default defineNuxtModule({
meta: { name: 'analytics' },
defaults: { id: '' },
setup(options, nuxt) {
const resolver = createResolver(import.meta.url)
nuxt.options.runtimeConfig.public.analyticsId = options.id
addPlugin(resolver.resolve('./runtime/plugin.client'))
},
})
Q26What are Nuxt plugins and how do they differ from modules?
IntermediatePlugins
Answer
A plugin is a file in `plugins/` that runs at app initialisation (once on the server during SSR, once in the browser at hydration). Use plugins to set up runtime singletons: configure a Vue plugin (Vue I18n, FloatingVue), instantiate a client (Posthog, Sentry, Stripe.js), or inject something into `useNuxtApp().$something` so every component can grab it. Naming controls when they run: `plugins/foo.client.ts` runs only in the browser, `plugins/foo.server.ts` only on the server, `plugins/foo.ts` runs on both.
To order plugins or wait for another, use `defineNuxtPlugin({ name, dependsOn, setup })`. The difference vs modules: **modules** extend Nuxt at build time (add routes, components, types, they don't ship runtime code on their own); **plugins** are runtime code that runs every time the app boots. A module often ships one or more plugins under the hood.
Two operational notes that come up in follow-ups. Plugins execute in alphabetical filename order, which is why numeric prefixes like `01.sentry.client.ts` are still common, though the explicit form is `defineNuxtPlugin({ name: 'sentry', dependsOn: ['auth'], setup })`. A plugin that throws during SSR takes the whole render down as a 500, so third-party SDK initialisation belongs in a try/catch that degrades rather than crashes the page.
Setting `parallel: true` stops a slow async plugin from blocking app boot, which is the right call for analytics that nothing else waits on. Anything returned under `provide` is typed on `useNuxtApp()` automatically because Nuxt writes the declaration into `.nuxt/types/`, so `$sentry` gets real autocomplete with no manual `declare module`. And plugins run before route middleware, which is precisely why session restoration goes in a plugin: the auth guard needs something to read by the time it fires.
// plugins/sentry.client.ts
export default defineNuxtPlugin((nuxtApp) => {
const { public: { sentryDsn } } = useRuntimeConfig()
const sentry = initSentry({ dsn: sentryDsn })
return { provide: { sentry } } // useNuxtApp().$sentry in any component
})
// any component
<script setup>
const { $sentry } = useNuxtApp()
$sentry.captureException(err)
</script>
Q27What is the difference between route middleware and `server/middleware/` in Nuxt 3?
IntermediateMiddleware
Answer
They live in different runtimes and solve different problems. Route middleware (`middleware/*.ts`, written with `defineNuxtRouteMiddleware`) is part of the vue-router lifecycle: it runs before a page renders, receives `to` and `from`, and can `return navigateTo()` or `abortNavigation()`. After hydration it executes entirely in the browser.
Server middleware (`server/middleware/*.ts`, written with `defineEventHandler`) runs inside Nitro on every incoming request, including `/api/*` calls and the SSR request for the page itself. It knows nothing about pages or routes-as-components, and if it returns a value the request short-circuits with that response instead of continuing. The practical split is that route middleware decides what the UI shows and server middleware enforces what the server allows, which is why an auth guard implemented only as route middleware is defeated by curling `/api/orders` directly.
Server middleware is also the right place to attach request-scoped context with `event.context.user = await getUser(event)`, so every downstream handler reads the session without re-parsing the cookie. Two gotchas: it also fires for health checks and asset requests, so filter on `event.path` early and keep the work cheap, and because it runs during SSR too, throwing in it produces a 500 error page rather than a redirect unless you explicitly call `sendRedirect(event, '/login', 302)`.
// server/middleware/auth.ts: runs on EVERY Nitro request
export default defineEventHandler(async (event) => {
if (!event.path.startsWith('/api/')) return // filter early, this is hot
if (event.path.startsWith('/api/public')) return
const token = getCookie(event, 'session')
const user = token && (await verifySession(token))
if (!user) throw createError({ statusCode: 401, statusMessage: 'Unauthorized' })
event.context.user = user // every downstream handler can read this
})
// server/api/orders.get.ts
export default defineEventHandler((event) => db.orders.findByUser(event.context.user.id))
// middleware/auth.ts: UI only, bypassable, still worth having
export default defineNuxtRouteMiddleware(() => {
const { loggedIn } = useUserSession()
if (!loggedIn.value) return navigateTo('/login')
})
Key Points
- Route middleware = router lifecycle, runs in the browser after hydration
- Server middleware = Nitro, runs on every request including /api
- Real authorization belongs in server middleware or the handler
- `event.context` carries the session to downstream handlers
Q28What changed in Nuxt 4 and how do you migrate a Nuxt 3 app to it?
IntermediateMigration
Answer
The headline change is the directory layout: client-side source moves under `app/` (`app/pages/`, `app/components/`, `app/composables/`, `app/app.vue`), while `server/`, `content/`, `public/` and `nuxt.config.ts` stay at the project root. That separation exists so the Vite and Nitro TypeScript projects stop overlapping, which fixes server types bleeding into client code and speeds up the file watcher on large repos. Data fetching gets stricter defaults. `useAsyncData` and `useFetch` no longer hand every caller of a key the same shared data ref, and the cached data is cleaned up when the last component using that key unmounts, which closes a genuine memory-growth path in long-lived apps.
Fetched data is also a `shallowRef` by default, so mutating a nested field no longer triggers a re-render and you must replace the object instead. The migration path is deliberately gentle: on a recent Nuxt 3 release you set `future: { compatibilityVersion: 4 }` in `nuxt.config.ts` and opt into the new behaviour before touching the package version, then run the official codemod recipe to move directories and apply mechanical rewrites. Budget your real time for the `shallowRef` change and for any component that relied on two `useAsyncData` calls sharing one ref, because those surface as silent UI bugs rather than build failures.
// nuxt.config.ts: opt in from Nuxt 3 before upgrading the package
export default defineNuxtConfig({
future: { compatibilityVersion: 4 },
})
// then run the official recipe to move files
// npx codemod@latest nuxt/4/migration-recipe
// Nuxt 3 -> Nuxt 4
// pages/ app/pages/
// components/ app/components/
// composables/ app/composables/
// app.vue app/app.vue
// server/ server/ (unchanged)
// data is a shallowRef in v4: replace the object, do not mutate a nested field
const { data } = await useFetch('/api/cart')
// data.value.items.push(item) // no re-render
data.value = { ...data.value, items: [...data.value.items, item] }
Key Points
- Client source moves under `app/`, server stays at the root
- `useAsyncData` keys are no longer shared refs and get cleaned up on unmount
- Fetched data is a `shallowRef`: replace, do not mutate
- `future.compatibilityVersion: 4` lets you migrate before upgrading
Q29How do you cache expensive work inside a Nitro server route?
IntermediateServer Routes
Answer
Beyond `routeRules`, Nitro gives you two primitives. `defineCachedEventHandler` wraps an entire handler and caches its response under a key you control with a `maxAge` in seconds. `cachedFunction` (exported as `defineCachedFunction` too) caches any async function, which is what you want when three routes all need the same expensive currency table or catalog lookup. Both write through `useStorage('cache')`, so moving from the filesystem default to Redis is a `nitro.storage` config change rather than a code change. Key design is where teams get this wrong.
The default key is derived from the full URL, so `?utm_source=newsletter` creates a separate entry and your hit rate quietly collapses to near zero; write an explicit `getKey(event)` that reads only the parameters that actually change the output. `swr: true` serves the stale value while refreshing behind the request, which keeps p99 flat when the upstream is slow, and without it every expiry produces a thundering herd of simultaneous misses against the same origin. Never cache a handler whose output depends on the session, because the first user's response is then served to everyone, the same failure class as ISR-caching a personalised page. If a route is only partly personalised, cache the expensive shared computation with `cachedFunction` and assemble the per-user response around it.
// server/api/rates.get.ts
export default defineCachedEventHandler(
async (event) => {
const { base } = getQuery(event)
return await $fetch('https://api.example.com/rates', { query: { base } })
},
{
maxAge: 60 * 10, // 10 minutes
swr: true, // serve stale, refresh behind
name: 'fx-rates',
getKey: (event) => String(getQuery(event).base ?? 'INR'), // ignore utm_* noise
},
)
// share one cached computation across several routes
const getCatalog = cachedFunction(
async (categoryId: string) => db.products.byCategory(categoryId),
{ maxAge: 300, name: 'catalog', getKey: (id: string) => id },
)
Q30What causes `[nuxt] A composable that requires access to the Nuxt instance was called outside of a plugin, Nuxt hook, Nuxt middleware, or Vue setup function`?
IntermediateDebugging
Answer
Nuxt composables read an async-local Nuxt instance that only exists synchronously inside setup, plugins, route middleware and Nuxt hooks. Any code that resumes after the microtask queue has moved on has lost that context. Three causes account for almost every occurrence: calling `useRuntimeConfig()` or `useState()` after an `await` in `<script setup>`; calling a composable inside a callback such as a click handler, a `setTimeout`, a `watch` callback or a `.then()`; and calling one at module scope in a plain `.ts` file that happens to be imported.
It shows up far more on the server, because Node interleaves concurrent requests through the same module instances while the browser only ever has one app instance, which is why the bug is often reported as intermittent. Fixes in order of preference: hoist the composable to the top of setup and close over the returned value, since `useRuntimeConfig()` returns a plain object you can use later; wrap the continuation in `nuxtApp.runWithContext(() => ...)` when you genuinely must call it after an await; or, inside a server route, use the event-scoped form `useRuntimeConfig(event)` instead of the app-level one. Nuxt improved context restoration across awaits in `<script setup>` specifically, so identical code can throw inside your own composable while working fine in a component.
<!-- BROKEN: the Nuxt instance is gone after the await -->
<script setup>
const data = await $fetch('/api/thing')
const config = useRuntimeConfig() // throws
</script>
<!-- FIX 1: call composables first, await afterwards -->
<script setup>
const config = useRuntimeConfig()
const data = await $fetch('/api/thing')
</script>
<!-- FIX 2: restore context explicitly when you must await first -->
<script setup>
const nuxtApp = useNuxtApp()
await somethingSlow()
const route = nuxtApp.runWithContext(() => useRoute())
</script>
// FIX 3: in a server route, use the event-scoped form
export default defineEventHandler((event) => useRuntimeConfig(event).public)
Key Points
- Nuxt context is async-local and only survives synchronous setup code
- Awaits, timers and event handlers drop it
- Hoist the call, or use `nuxtApp.runWithContext()`
- In server routes pass the event: `useRuntimeConfig(event)`
Q31How do you optimize a Nuxt 3 app for production performance?
AdvancedPerformance
Answer
Highest-impact items in order. (1) **Pick the right rendering mode per route**, use SSG/ISR for content pages, SPA for behind-auth admin, full SSR only when you need per-request personalisation. Wrong mode is the #1 cause of slow Nuxt apps. (2) **Lazy-load components**, `<LazyMyHeavyComponent />` (prefix with `Lazy`) defers loading until the component is rendered. Combine with `v-if` to keep them out of the initial bundle. (3) **Component islands / server components**, Nuxt 3.10+ supports `<MyComponent.server />` that renders only on the server and ships zero JS for that subtree, perfect for static parts of an otherwise interactive page. (4) **`@nuxt/image`** for responsive images: auto WebP/AVIF, lazy loading, CDN-friendly URLs (Cloudflare Images, IPX). (5) **Configure Nitro caching**, `routeRules` with `swr` or `isr` for any high-traffic route. (6) **Bundle analyse**, `nuxi analyze` shows you the biggest chunks; lazy-load anything > 50KB. (7) **Nuxt 4 RC's lazy hydration**, hydrate components on visibility/interaction instead of all at once.
The combination cuts TTI dramatically on content-heavy pages. For a BlueStone-style product page, you can hit a Lighthouse perf score of 95+ with these techniques alone.
Key Points
- Right rendering mode per route
- <Lazy> prefix + v-if for heavy components
- Server components for static interactive subtrees
- @nuxt/image with WebP/AVIF
- nuxi analyze + Nuxt 4 lazy hydration
Q32How does Nitro work and why is it called a 'server engine'?
AdvancedServer
Answer
Nitro is the server-side runtime Nuxt 3 is built on, it's also a standalone framework (UnJS ecosystem). 'Server engine' (vs server framework) means it's deployment-target agnostic: the same Nuxt code can be built to a Node server, a serverless function (Vercel, Netlify, AWS Lambda), an edge worker (Cloudflare Workers, Deno Deploy), or a static site, just by changing the `nitro.preset` config. Nitro handles: HTTP routing for `server/api/`, server-side rendering, automatic code-splitting per route, route-level caching (`routeRules`), middleware composition, and a unified storage abstraction (`useStorage()` that works across filesystem, Redis, S3, etc.).
The 'isomorphic deployment' story is the big reason Nuxt won mind-share for edge-first apps in 2024-2025, the same codebase can run on Cloudflare Workers for low-latency global delivery and on a regular VPS for control. Nitro also powers ofetch (the `$fetch` you use), unimport (auto-imports), and h3 (the underlying HTTP framework). Two specifics land well in interviews.
The build output at `.output/` is fully self-contained: `.output/server/index.mjs` has its dependencies bundled in, so a production image needs Node plus that folder and no `npm install` step, which is why Nuxt Docker images stay small and boot fast. And `useStorage()` is what makes preset portability honest, since `useStorage('cache').setItem()` hits the filesystem locally and Redis or Cloudflare KV in production purely from `nitro.storage` config, with no code change. The catch to raise when someone proposes an edge preset like `cloudflare-module` or `deno-deploy`: there is no full Node API surface there, so `fs`, native database drivers such as `mysql2`, and libraries leaning on `Buffer` or long-lived TCP connections either fail at build time or blow up on the first request. Those calls have to move behind a separate service or an HTTP-based driver.
// nuxt.config.ts: one codebase, many deployment targets
export default defineNuxtConfig({
nitro: {
preset: process.env.NITRO_PRESET || 'node-server', // vercel | cloudflare-module | aws-lambda | bun
storage: { cache: { driver: 'redis', url: process.env.REDIS_URL } },
prerender: { routes: ['/', '/pricing'], crawlLinks: true },
},
})
// npx nuxi build -> self-contained .output/
// node .output/server/index.mjs
// server/api/hit.get.ts: same code on every preset
export default defineEventHandler(async () => {
const store = useStorage('cache')
const hits = ((await store.getItem<number>('hits')) ?? 0) + 1
await store.setItem('hits', hits)
return { hits }
})
Q33How would you architect a high-traffic e-commerce site on Nuxt 3?
AdvancedArchitecture
Answer
A real-world e-commerce architecture for 50k DAU+ in India (think Bewakoof, BlueStone, Tata CLiQ scale): **Rendering**, product list pages get ISR with `swr: 600` (stale-while-revalidate 10 min) backed by Redis cache; product detail pages also ISR with on-demand revalidation triggered by CMS webhooks; cart and checkout pages use SSR for personalisation; account pages are SPA mode. **Data layer**, `useFetch` for static-ish data, Pinia store for cart state (persisted to localStorage on the client and to a session cookie on the server). **Server routes**, Nitro handlers that proxy to an internal Express/FastAPI service for the actual commerce logic (catalog, inventory, payments). **Images**, `@nuxt/image` with Cloudinary or an IPX server; lazy-loaded, AVIF-first. **Search**, Algolia or a Meilisearch instance; the search page is SPA for instant results. **Deployment**, Nitro built to Node, hosted on a Kubernetes cluster (typical in India), with Cloudflare in front for global CDN and DDoS. **Observability**, Sentry for browser + server errors, OpenTelemetry traces shipped to SignOz/Datadog, structured logs to ELK. **Caching strategy**, Cloudflare cache for HTML (24h with on-demand purge), Redis for session and cart state, browser cache for `_nuxt/` immutable assets (1y).
Q34How do you implement on-demand ISR revalidation in Nuxt 3?
AdvancedRendering
Answer
Two pieces: configure ISR for the routes, then expose a webhook that purges the cache when the underlying data changes. In `nuxt.config.ts`, set `routeRules: { '/products/**': { isr: true } }`, `true` (not a TTL number) means cache indefinitely until purged. Then add a server route that the CMS/admin calls when a product is updated.
The route uses Nitro's storage API to delete the cached entry. For self-hosted deployments, plug Redis as the cache driver in `nitro.storage`. For Vercel/Netlify deployments, use their on-demand revalidation primitives (Vercel: `purgeCache` from `@vercel/cache-api`, Netlify: their `revalidate` API).
Gotcha: in development mode, ISR is disabled, test against a production build (`nuxi build && nuxi preview`). For a Tata CLiQ-style scale, you'd also want a fan-out: when a price changes, purge not just the product page but also the category pages it appears on. Maintain a mapping of `productId → affectedPaths` and loop the purge over all of them.
Two more things get checked. Purging Nitro's cache is not the same as purging the CDN in front of it: with Cloudflare or Fastly caching the HTML at the edge, a Nitro-only purge leaves stale pages served for hours, so the webhook has to call the platform purge API as well. And the endpoint must be authenticated and rate limited, because an open revalidate route is a free way for anyone to force thousands of full re-renders and take the origin down. In practice teams push purge jobs onto a queue rather than looping inline, since a category-wide price change fans out to thousands of paths and a synchronous loop will exceed the webhook timeout and get retried, doubling the load.
// server/api/revalidate.post.ts
import { defineEventHandler, readBody, createError } from 'h3'
export default defineEventHandler(async (event) => {
const { secret, paths } = await readBody(event)
if (secret !== process.env.REVALIDATE_SECRET) {
throw createError({ statusCode: 401 })
}
const cache = useStorage('cache')
for (const path of paths) {
await cache.removeItem(`nitro:routes:${path}`)
}
return { revalidated: paths }
})
Q35How do you compare Nuxt 3 vs Next.js for a new project in 2026?
AdvancedArchitecture
Answer
Both are excellent in 2026 and the choice is mostly about ecosystem and team. **Pick Next.js if**, your team writes React, you want the biggest ecosystem (every UI library has a React version first), you're shipping to Vercel and want zero-friction (Next is the home team there), or you need React Server Components specifically (Next's `app/` directory has the most mature RSC implementation; Nuxt's server components are good but newer). **Pick Nuxt 3 if**, your team writes Vue, you want a saner DX (auto-imports, less ceremony, single-file components are nicer for most devs than JSX), you're deploying to multiple runtimes (Nuxt's Nitro preset story is more flexible than Next's), or you need a tightly-integrated content layer (`@nuxt/content` beats `next/mdx` for content-heavy sites). **Performance**, comparable; Vue's reactivity is lighter than React's vDOM diffing, but Next's RSC pushes more work to the server. **Hiring in India**, React/Next dominates; Vue/Nuxt is niche but well-paid for specialists, and there's less competition for jobs. **Stability**, both are battle-tested in 2026. Don't pick Nuxt if your team isn't already on Vue; the learning curve isn't worth it for a marginal DX win. Don't pick Next if you genuinely prefer Vue's ergonomics, fighting JSX for two years isn't fun.
Key Points
- Same capability surface in 2026
- Next.js wins on ecosystem size and RSC maturity
- Nuxt 3 wins on DX, deployment flexibility, content tooling
- Pick the framework that matches your view library
Q36How do Nuxt server components and islands work, and where do they fall short?
AdvancedPerformance
Answer
A server component is a `.server.vue` file that Nuxt renders only on the server and delivers as HTML through the island mechanism: the browser receives markup plus a small JSON payload and downloads zero component JavaScript for that subtree. `<NuxtIsland name="Foo" :props="...">` is the lower-level API, and an island can be re-fetched on demand, which lets you refresh a server-rendered fragment without ever hydrating it. You enable the feature with `experimental: { componentIslands: true }` in `nuxt.config.ts`. The payoff is real on content-heavy pages: a long markdown article, a syntax-highlighted code block, or a spec table can carry a heavy dependency (a Shiki highlighter, a markdown renderer, a date-formatting library) that never crosses the wire.
The limits are what a senior interviewer actually probes. Server components have no client instance, so no `onMounted`, no browser reactivity, no event handlers. Slot support is restricted, and passing interactive children into an island requires the `nuxt-client` attribute and is still flagged experimental.
Props travel as JSON in the island payload, so they must be serialisable. And every island refresh is a server round trip, which means using one for something that changes on hover is strictly worse than hydrating it. The rule of thumb: islands for expensive-but-static, hydration for interactive.
// nuxt.config.ts
export default defineNuxtConfig({
experimental: { componentIslands: true },
})
<!-- components/ArticleBody.server.vue: Shiki + markdown stay on the server -->
<script setup lang="ts">
const props = defineProps<{ markdown: string }>()
const html = await renderMarkdownWithShiki(props.markdown)
</script>
<template>
<div class="prose" v-html="html" />
</template>
<!-- pages/blog/[slug].vue -->
<template>
<ArticleBody :markdown="post.body" />
<CommentBox /> <!-- ordinary component: hydrates, stays interactive -->
</template>
Key Points
- `.server.vue` ships HTML with zero component JS
- Props must be JSON-serialisable, no client lifecycle hooks
- Islands refresh over the network, so not for hover-speed interactions
- Best fit: heavy render dependencies on static content
Q37How does delayed (lazy) hydration work in recent Nuxt versions, and when does it backfire?
AdvancedPerformance
Answer
Delayed hydration lets a `Lazy`-prefixed component render its server HTML immediately while postponing both the chunk download and the attachment of Vue reactivity until a trigger fires. The strategies are props on the component: `hydrate-on-visible` (IntersectionObserver, accepts `{ rootMargin }`), `hydrate-on-idle` (requestIdleCallback with an optional timeout), `hydrate-on-interaction` (pointerenter and focus by default, or name specific events), `hydrate-on-media-query`, `hydrate-after` for a plain millisecond delay, `hydrate-when` for an arbitrary boolean, and `hydrate-never` for a subtree that is genuinely static after render. It shipped in Nuxt 3.16 and is a first-class path in Nuxt 4.
The mechanism is what makes it worth doing: the JavaScript chunk is only fetched when the trigger fires, so you cut both bytes and main-thread work, which moves TBT and INP rather than just LCP. Where it backfires: a component hydrated on interaction has a real gap between the first click and the handler existing, so on a slow connection the click is swallowed unless you show a pending state. `hydrate-on-visible` on above-the-fold content is pure overhead, since the observer fires immediately anyway. And any component whose server HTML differs from its eventual client state (a cart badge, a personalised greeting) will visibly flip when hydration finally lands. Verify with Lighthouse or WebPageTest, not by feel.
<template>
<!-- above the fold: hydrate normally -->
<HeroBanner />
<!-- heavy chart: only when the user scrolls near it -->
<LazyRevenueChart :hydrate-on-visible="{ rootMargin: '200px' }" :data="rows" />
<!-- non-critical: wait for an idle frame, 3s ceiling -->
<LazyRecommendations :hydrate-on-idle="3000" />
<!-- becomes interactive on hover or focus -->
<LazyMegaMenu hydrate-on-interaction="pointerenter" />
<!-- desktop only -->
<LazyDesktopFilters hydrate-on-media-query="(min-width: 1024px)" />
<!-- static after SSR: never ship the JS at all -->
<LazyFooterLinks hydrate-never />
</template>
Q38How do you deploy the same Nuxt 3 app to a Node VPS, AWS Lambda, and Cloudflare Workers, and what breaks on each?
AdvancedDeployment
Answer
One `nitro.preset` value (or the `NITRO_PRESET` environment variable) picks the target and the application code is mostly unchanged, but each runtime has its own failure profile. **Node VPS or Kubernetes** (`node-server`): full Node API, long-lived database connections, a warm in-process cache, and you own the scaling. Serve `.output/public/` from a CDN rather than through Node, and set `NITRO_PORT` and `NITRO_HOST`. **AWS Lambda** (`aws-lambda`): cold starts add a few hundred milliseconds on the first request per instance, the filesystem cache driver is per-instance and evaporates, and connection pools are the classic disaster because every concurrent instance opens its own, exhausting the database. Use RDS Proxy or an HTTP data API and move the Nitro cache to Redis or DynamoDB. **Cloudflare Workers** (`cloudflare-module`): there is no full Node API surface, so `fs`, `mysql2`, `sharp` and most native modules fail at build or on first request; you need `nodejs_compat` in `wrangler.toml` for the polyfilled subset, KV or D1 for storage, and you are working against a CPU-time budget per request, so rendering a very large page can hit the ceiling. Two gotchas apply everywhere: `runtimeConfig` overrides are read from environment variables at boot, so an image built with inline `process.env` reads is not portable between environments, and any code assuming a writable local disk (uploads, temporary PDFs) has to move to S3 or R2 before you go serverless.
// nuxt.config.ts
export default defineNuxtConfig({
nitro: {
preset: process.env.NITRO_PRESET || 'node-server',
storage: {
// the filesystem default is per-instance: unusable on Lambda or Workers
cache: { driver: 'redis', url: process.env.REDIS_URL },
},
},
})
# Node VPS / Kubernetes
NITRO_PRESET=node-server npx nuxi build
NITRO_PORT=3000 node .output/server/index.mjs
# AWS Lambda (zip .output and hand it to your IaC)
NITRO_PRESET=aws-lambda npx nuxi build
# Cloudflare Workers
NITRO_PRESET=cloudflare-module npx nuxi build && npx wrangler deploy
Key Points
- `nitro.preset` is the only code-level switch between targets
- Lambda: cold starts, no shared cache, connection-pool exhaustion
- Workers: no Node API, CPU budget per request, KV or D1 for storage
- Never rely on local disk or in-process cache once you go serverless
Q39A Nuxt SSR pod's memory climbs all day and TTFB degrades until it restarts. How do you diagnose it?
AdvancedDebugging
Answer
First separate render time from upstream time, because slow TTFB usually is not Vue. Add a Nitro plugin that hooks `request` and `afterResponse` to log duration per path, or wire OpenTelemetry so the SSR span sits next to the database and API spans. If the render span is flat while total time grows, the problem is a client library or an upstream, not Nuxt.
For the memory side, run with `node --inspect .output/server/index.mjs` and diff two heap snapshots, or use `--heapsnapshot-signal=SIGUSR2` in production and take snapshots hours apart. The Nuxt-specific culprits, in the order they usually turn out to be true: module-scope state in a composable or plugin (`const cache = new Map()` at the top of a file), which is created once per Node process and therefore grows across every request forever; listeners or intervals registered in a `.server.ts` plugin with no teardown; a `cachedFunction` or `useStorage('cache')` on the in-memory driver with no `maxAge`, which is an unbounded map by definition; and arrays pushed onto `event.context` that nothing ever clears. Confirm you are not accidentally running `nuxi dev` in production, since it keeps source maps and the full Vite module graph resident. Mitigations while you hunt: cap the heap with `--max-old-space-size`, add a readiness probe that fails on high RSS so the orchestrator recycles the pod, and put a short `swr` window on the heaviest routes so fewer requests reach the renderer at all.
// server/plugins/timing.ts: render time vs total time
export default defineNitroPlugin((nitroApp) => {
nitroApp.hooks.hook('request', (event) => {
event.context._start = performance.now()
})
nitroApp.hooks.hook('afterResponse', (event) => {
const ms = performance.now() - (event.context._start ?? 0)
if (ms > 500) console.warn('[slow]', event.method, event.path, Math.round(ms), 'ms')
})
})
// the classic leak: module scope is shared by every SSR request in the process
const seen = new Map() // never freed, grows all day
export const useTracker = (id: string) => { seen.set(id, Date.now()) }
# heap diffing against a running pod
# node --heapsnapshot-signal=SIGUSR2 .output/server/index.mjs
# kill -SIGUSR2 <pid> (take two hours apart, diff them in DevTools)
Key Points
- Instrument `request` / `afterResponse` before guessing
- Module-scope state is per-process, not per-request: the usual leak
- Unbounded `cachedFunction` or memory-driver storage grows forever
- Cap heap, add an RSS-based readiness probe, add `swr` to hot routes
Q40How do you get end-to-end type safety between `server/api/` handlers and `$fetch` on the client?
AdvancedTypeScript
Answer
Nuxt does most of this automatically. During `nuxi prepare` and `nuxi dev`, Nitro scans `server/api/` and `server/routes/` and writes a route map into `.nuxt/types/nitro-routes.d.ts` that augments the `InternalApi` interface. Because `$fetch`, `useFetch` and `useAsyncData` are typed against that map, `const { data } = await useFetch('/api/products/1')` infers the handler's return type with no generics and no shared DTO package, and a typo in the path becomes a compile error instead of a runtime 404.
The catches are worth naming. The inferred type is the serialised type, so a `Date` returned by a handler arrives typed as `string`, and anything non-JSON degrades the same way, which is a good argument for returning ISO strings and integers (prices in paise) deliberately. Dynamic segments only resolve when the path literal is inferable, so a template string assembled from variables falls back to a loose type.
Additions to `event.context` cannot be inferred at all and need a manual `declare module 'h3'` augmentation. And the types go stale whenever routes change while the dev server is off, which is why CI should run `nuxi prepare` before `vue-tsc`. Pair all of this with `readValidatedBody(event, Schema.parse)`, because inference gives you nothing at the runtime boundary where untrusted input actually arrives.
// server/api/products/[id].get.ts
export default defineEventHandler(async (event) => {
const id = getRouterParam(event, 'id')!
return { id, name: 'Ring', priceInPaise: 129900, createdAt: new Date() }
})
// client: the return type is inferred from the handler, no generics needed
const { data } = await useFetch('/api/products/1')
data.value?.priceInPaise // number
data.value?.createdAt // string, because the payload is serialised
// server/api/cart.post.ts: validate the input side at runtime
import { z } from 'zod'
const Body = z.object({ sku: z.string(), qty: z.number().int().positive() })
export default defineEventHandler(async (event) => {
const { sku, qty } = await readValidatedBody(event, Body.parse)
return { sku, qty }
})
// types for anything you attach in server middleware
declare module 'h3' {
interface H3EventContext { user?: { id: string } }
}
Key Points
- `.nuxt/types/nitro-routes.d.ts` augments `InternalApi` for `$fetch`
- Inferred types are the serialised shape: `Date` becomes `string`
- `event.context` needs a manual `declare module 'h3'`
- Run `nuxi prepare` before `vue-tsc` in CI or types go stale
Frequently Asked Questions
Is Nuxt.js worth learning in 2026 given Next.js dominance?
Yes if your team or target employer is on Vue, Nuxt is the de-facto Vue meta-framework and there's no real alternative in that ecosystem. No if you'd be the only Vue person at a React shop. The skill is genuinely transferable: most of what you learn (SSR vs SSG, hydration, server routes, route rules) applies to Next.js conceptually too.
How much does a Nuxt.js developer earn in India?
₹6-20 LPA in 2026 for mid-to-senior frontend roles with Nuxt as a primary skill. Lower end (₹6-10 LPA) for 1-3 years experience at agencies and D2C brands; upper end (₹15-20 LPA) for senior engineers at product companies (BlueStone, Mosaic Wellness, Tata CLiQ). Specialised Nuxt + e-commerce + Magento-migration experience can clear ₹25 LPA at top product cos. Demand is lower than React/Next but so is supply, so good Nuxt devs find work quickly.
Should I learn Nuxt 3 or wait for Nuxt 4?
Learn Nuxt 3 now. The migration from 3 to 4 is small, mostly defaults changing and a few API tweaks. Nuxt 4 RC mostly adds lazy hydration and bundle improvements; nothing in 3 is being deprecated. Most of the public Nuxt 4 features are also flag-gated in late Nuxt 3 versions, so you can preview them without waiting.
What's the difference between Nuxt 2 and Nuxt 3?
Big jump. Nuxt 2 = Vue 2 + Webpack + Options API + Vuex; Nuxt 3 = Vue 3 + Vite + Composition API + Pinia + Nitro server engine. Auto-imports, TypeScript-first, native SSG/SSR/ISR/SPA hybrid mode, edge deployment, none of that existed in Nuxt 2. If you're maintaining a Nuxt 2 app in 2026, the migration to Nuxt 3 is a near-rewrite for non-trivial codebases; budget weeks, not days. Nuxt 2 reached EOL on 30 June 2024.
Can I use Tailwind CSS with Nuxt 3?
If you already write Vue 3 with the Composition API, two to three weeks of evenings is realistic: one week on rendering modes and `routeRules`, one on `useFetch`/`useAsyncData`/`$fetch` and their caching behaviour, and a few days building one small app with `server/api/` routes, cookie auth and an actual deploy. Coming from React and Next.js, add roughly two weeks for Vue itself (reactivity, `ref` vs `reactive`, single-file components, slots), because interviewers test Vue fundamentals harder than Nuxt trivia. The highest-yield exercise by far is shipping one SSR app to production and debugging one real hydration mismatch, that single experience covers about a third of the questions on this page.
What do interviewers expect from a fresher versus a 4+ year engineer in a Nuxt role?
A fresher is asked to explain file-based routing, `useFetch` versus `$fetch`, layouts and `useState`, and usually to build a small page live; describing SSR versus SPA correctly in plain words is enough to pass. At three to five years the bar moves to judgement: which rendering mode for which route and why, how to cache a product page without leaking a logged-in user's data into it, how to debug a hydration mismatch that only appears in production, and how the thing is deployed. Beyond five years you get architecture and cost questions (Nitro presets, Redis-backed caching, purge fan-out when a catalog price changes) plus migration planning, such as moving a Nuxt 2 codebase forward or opting a Nuxt 3 app into the Nuxt 4 `app/` layout without freezing feature work.
Should I list Nuxt.js or Vue.js on my resume, and how does it compare to Next.js for job hunting in India?
List Vue.js as the core skill and Nuxt.js as the framework, because most Indian job descriptions are written to search for Vue first and Nuxt second. Postings naming React and Next.js outnumber Nuxt postings by a wide margin, so if you are optimising purely for interview volume, React plus Next.js is the higher-traffic path. Nuxt roles are fewer, but the applicant pool is much smaller, so a profile with real SSR, Nitro and e-commerce performance work gets shortlisted faster and negotiates from a better position. The pragmatic move for a 2026 search: keep Vue and Nuxt as your depth, and be fluent enough in the Next.js equivalents (the `app/` router, server components, `revalidate`) that a React shop cannot screen you out on vocabulary alone.
Can I use Tailwind CSS with Nuxt 3?
Yes, `@nuxtjs/tailwindcss` is one of the most-installed Nuxt modules. Add it to `modules` in `nuxt.config.ts` and you're done; the module wires up the PostCSS config, generates the Tailwind config file on first run, and integrates with HMR. Alternatives in 2026: UnoCSS via `@unocss/nuxt` (atomic CSS, smaller bundles), and Nuxt UI (Tailwind-based component library built by the Nuxt team).
Introduction
Nuxt.js is the production-grade meta-framework for Vue 3, what Next.js is to React. In 2026, Nuxt 3 is the stable line (3.14+), built on Vite for dev, Nitro for the server runtime, and Vue 3 Composition API at the component layer. Nuxt 4 is in late RC with lazy hydration and a smaller bundle baseline, and shops are starting to ship it in greenfield projects.
In India, Nuxt is a niche but real hire, most demand comes from e-commerce shops migrating off Magento or PHP storefronts (Tata CLiQ, BlueStone, Bewakoof, Mosaic Wellness brands), Bangalore product agencies (smashing-magazine-style boutique studios), and a long tail of D2C brands on Shopify Hydrogen alternatives. Outside India, Louis Vuitton, BMW, and OpenAI's marketing site all run on Nuxt. Expect salaries in the ₹6-20 LPA range, a touch lower than Next.js on average, but senior Vue/Nuxt specialists at top product cos can clear ₹25-30 LPA.
If you're interviewing for a Nuxt role in 2026, the questions cluster around: rendering modes (SSR vs SSG vs hybrid vs ISR), the data-fetching composables (useFetch, useAsyncData, $fetch) and their caching quirks, server routes on Nitro, state management with Pinia + useState, and hydration-mismatch gotchas. Senior loops go further: Nitro caching (`defineCachedEventHandler`, `routeRules`, `useStorage`), deployment presets and what breaks on Lambda or Cloudflare Workers, server components and islands, delayed hydration, `@nuxt/test-utils`, and the Nuxt 4 `app/` directory migration. This guide walks through the 40 questions you're most likely to hit, ordered basic first, then intermediate, then advanced.
Ready to practice Nuxt.js interviews?
Don't just read, practice these Nuxt.js questions live with an AI interviewer that asks follow-ups and scores your answers.