Skip to content

Top 31 Nuxt Interview Questions and Answers [2026]

November 10, 2026 30 min read
Nuxt interview guide by Second Talent
TL;DR: Nuxt interviews in 2026 test whether a candidate can fetch data once on the server and reuse it on the client, pick a rendering mode per route, and write Nitro server routes that will survive the move to Nuxt 5. Candidates should know Nuxt 4.6 (4.6.0 shipped October 5, 2026; 4.6.4 on November 2, 2026), the new nuxt/server imports, Nuxt 3's end of life on July 31, 2026, and the fact that Nitro v3 is still a release candidate.

Nuxt 4.6 added nuxt/server, an import surface for server handlers that runs unchanged on today's Nitro v2 and on the Nitro v3 and h3 v2 stack planned for Nuxt 5, so server code written this quarter decides how painful next year's upgrade will be.

The current release line is Nuxt 4.6, with 4.6.4 as the latest patch. The questions below are for mid-level and senior Nuxt and Vue developer interviews, with four find-the-bug rounds and two design questions on rendering and caching.

Key takeaways
  1. 1Nuxt 3 reached end of life on July 31, 2026, and 4.6.0 was the first minor with no 3.x release alongside it.
  2. 2In Nuxt 4, data from useFetch is a shallow ref by default (deep: false), so mutating a nested field does not re-render.
  3. 3Nuxt 4.6 still ships nitropack 2.13 and h3 v1; Nitro v3 entered release candidate on October 29, 2026 and is not yet stable.
  4. 4The rebuilt typed $fetch in 4.6 type-checks a 1,000-route app in 0.20s instead of 7.44s, per the release benchmarks, and stops the TS2589 error.

Nuxt 4 Fundamentals

1. How is a Nuxt 4 project laid out, and what moved from Nuxt 3?

Application code lives in app/ by default: app/pages, app/components, app/layouts, app/composables, app/middleware, app/plugins and app/app.vue. server/, shared/, public/, modules/ and nuxt.config.ts stay at the root.

Per the upgrade guide, the default srcDir is now app/, serverDir defaults to <rootDir>/server, and the ~ alias points at app/.

my-app/
  app/
    components/
    composables/
    layouts/
    middleware/
    pages/
    plugins/
    app.vue
  server/
    api/
    middleware/
    plugins/
    utils/
  shared/
  public/
  nuxt.config.ts

A strong candidate explains why: file watchers no longer scan node_modules and .git, and the split gives each context its own TypeScript project.

Nuxt 4 generates separate projects for app code, server code, shared/ and builder code, so importing a server-only module from a component is a type error, not a runtime surprise.

They should also know the escape hatch: setting srcDir: '.' keeps the Nuxt 3 layout, and Nuxt detects an unmigrated project on its own.

2. What is the Nuxt context, and why do composables fail after an await?

Nuxt composables such as useNuxtApp, useRuntimeConfig, useState and useRoute read the current Nuxt app instance from an async context.

That context exists synchronously inside <script setup>, plugins, route middleware and defineNuxtComponent. Call one in a plain function after an await, in a setTimeout, or at module scope, and the instance may be gone.

Since 4.5 that failure carries the stable error code NUXT_E1001 with an explanation and a suggested fix. The fixes a candidate should name:

  • Call the composable before the first await and keep the result in a local.
  • Wrap later calls in nuxtApp.runWithContext(() => ...).
  • Do the work inside useAsyncData, whose handler receives the Nuxt app as its first argument.

Nuxt's compiler transforms <script setup>, definePageMeta and defineNuxtPlugin to restore the context across awaits, which is why the bug appears in hand-written helpers and not in components.

Nuxt 4.5 also moved to unctx v3, which fixed a class of async context bugs.

3. Find the bug: a shared cart composable.

// app/composables/useCart.ts
const items = ref<CartItem[]>([])

export function useCart() {
  const add = (item: CartItem) => items.value.push(item)
  const total = computed(() =>
    items.value.reduce((sum, i) => sum + i.price * i.qty, 0))
  return { items, add, total }
}

Bug: the ref is created at module scope. On the server, modules load once per process, so every request shares one array. User A's cart items appear in the HTML rendered for user B, and the array grows until the process restarts.

The state management docs say it directly: never define const state = ref() outside <script setup> or setup().

Fix: useState creates state per request on the server and serializes it into the payload for hydration.

export function useCart() {
  const items = useState<CartItem[]>('cart', () => [])
  const add = (item: CartItem) => { items.value = [...items.value, item] }
  const total = computed(() =>
    items.value.reduce((sum, i) => sum + i.price * i.qty, 0))
  return { items, add, total }
}

The follow-up that separates seniors: useState values go through the payload as JSON, so classes, functions and symbols do not survive.

A Pinia store solves the same per-request problem with more structure, and a candidate who suggests Pinia should know that its @pinia/nuxt module handles the per-request store creation for them.

4. runtimeConfig or app.config: which one holds what?

runtimeConfig holds values that change per environment and can be overridden by environment variables at runtime; app.config.ts holds public values fixed at build time, such as theme settings or feature labels.

Keys under runtimeConfig are server-only unless placed under public, and a key such as githubToken is overridden at runtime by NUXT_GITHUB_TOKEN.

// nuxt.config.ts
export default defineNuxtConfig({
  runtimeConfig: {
    githubToken: '',          // server only, NUXT_GITHUB_TOKEN
    public: {
      apiBase: '/api',        // client too, NUXT_PUBLIC_API_BASE
    },
  },
})

A weak answer misses two traps. The environment variable only overrides a key that exists in nuxt.config, so a missing default means the variable is ignored.

Values read with process.env inside nuxt.config are baked in at build time, which breaks a build-once, deploy-many pipeline.

Nuxt 4.6 added a reserved runtimeConfig.appSecret (set with NUXT_APP_SECRET), and the upgrade guide warns that on Nitro v2 a digits-only secret arrives as a number.

5. In what order do plugins and route middleware run?

Plugins run once per app instance, on the server per request and on the client at startup, in file-name order inside app/plugins/ after module-registered plugins.

Route middleware runs on every navigation: global middleware (files ending in .global.ts) first, alphabetically by file name, then the middleware a page declares in definePageMeta, in array order.

  • A numeric prefix such as 01.auth.ts forces plugin and global middleware order (file names sort as strings, so 10.x comes before 2.x), and the object form of defineNuxtPlugin accepts enforce, dependsOn and parallel for finer control.
  • Middleware returns navigateTo('/login') to redirect or abortNavigation() to stop; returning nothing continues.
  • On the first load, middleware runs on the server, and runs again on the client during hydration unless the code checks import.meta.server or the route is client-only.

Route middleware in app/middleware and server middleware in server/middleware are different things.

Server middleware runs on every request to Nitro, including API calls and assets, and should only add context or headers, never return a response for normal routes.

Data Fetching

6. When do you use useFetch, useAsyncData and $fetch?

Use useFetch to load data for a component from a URL during render, useAsyncData to wrap any other async source (an SDK, several calls, a CMS client) the same way, and $fetch for calls made in response to user actions.

The first two put the server result into the Nuxt payload, so the client hydrates from it instead of calling the API again. useFetch(url, opts) is shorthand for useAsyncData with a generated key and $fetch, and it infers the response type from your server routes.

<script setup lang="ts">
// Rendering data: fetched on the server, reused on hydration
const { data: posts, status, error } = await useFetch('/api/posts')

// Several sources under one key
const { data: dashboard } = await useAsyncData('dashboard', async (_nuxtApp, { signal }) => {
  const [user, stats] = await Promise.all([
    $fetch('/api/me', { signal }),
    $fetch('/api/stats', { signal }),
  ])
  return { user, stats }
})

// Event handler: plain $fetch, no payload involved
async function like(id: number) {
  await $fetch(`/api/posts/${id}/like`, { method: 'POST' })
}
</script>

The classic mistake is a bare await $fetch() in <script setup>.

It runs on the server during SSR and again in the browser during hydration, because nothing stores the result, so every page load hits the API twice and risks a hydration mismatch if the data changed between calls.

7. Find the bug: the profile page shows "logged out" on refresh.

<!-- app/pages/profile.vue -->
<script setup lang="ts">
const { data: me } = await useAsyncData('me', () => $fetch('/api/me'))
</script>

<template>
  <p v-if="me">Signed in as {{ me.email }}</p>
  <p v-else>Please sign in</p>
</template>

Bug: during SSR, $fetch('/api/me') is a server-to-server call that does not carry the browser's cookies, so /api/me sees an anonymous request and returns 401 or an empty user.

Client-side navigation works, because the browser sends cookies itself, which makes the bug look intermittent. Per the data fetching docs, Nuxt does not forward request headers from plain $fetch during SSR for security reasons.

Fix: use useFetch('/api/me'), which uses useRequestFetch under the hood, or call const requestFetch = useRequestFetch() and use it inside the handler. Nuxt strips hop-by-hop headers such as host, connection and accept when forwarding.

Inside a server route the equivalent is event.$fetch. A senior candidate also asks whether the page should be cached at all: per-user HTML must not sit behind an swr or isr rule.

8. How do keys work, and what happens when two components use the same key?

The key identifies the data in the payload and in the shared cache. If you omit it, Nuxt generates one from the file name and line number.

In Nuxt 4, all calls with the same key share the same data, error, status and pending refs, and Nuxt removes the data when the last component using it unmounts.

Because the refs are shared, some options must match across calls with the same key: the handler, deep, transform, pick, getCachedData, default and middleware. Nuxt warns in development if they conflict.

Options such as server, lazy, immediate, dedupe, watch, enabled and serialize may differ.

Keys can be reactive. A computed key such as () => `user-${route.params.id}` stores each user separately and refetches when the route changes.

A strong candidate mentions the Nuxt 4 rule for immediate: false: a key change only triggers a fetch if the data has already been fetched once, and useFetch now follows the same rule as useAsyncData.

9. Find the bug: a todo checkbox that never updates.

<script setup lang="ts">
const { data: todo } = await useFetch('/api/todos/1')

async function toggle() {
  todo.value!.done = !todo.value!.done
  await $fetch('/api/todos/1', { method: 'PATCH', body: { done: todo.value!.done } })
}
</script>

<template>
  <label>
    <input type="checkbox" :checked="todo?.done" @change="toggle" />
    {{ todo?.title }}
  </label>
</template>

Bug: in Nuxt 4, data is a shallowRef because deep defaults to false.

Assigning todo.value.done mutates a property inside the object without replacing .value, so Vue does not track the change and anything bound to todo.done keeps its old value.

Code ported from Nuxt 3 tutorials, where data was deep, breaks this way.

Fix: replace the object, todo.value = { ...todo.value!, done: !todo.value!.done }, or pass deep: true for this call.

A better answer also handles failure: apply the change optimistically, restore the old object if the PATCH throws, or call refresh() after the write so the server stays the source of truth.

10. What do lazy, await and status change about navigation?

On the server, Nuxt waits for every useFetch and useAsyncData call before rendering, with or without await, so the HTML always contains the data. The difference shows on client-side navigation.

With await and the default lazy: false, navigation blocks until the data is ready, which Nuxt implements with <Suspense>. With lazy: true (or useLazyFetch), the route renders at once and you show a loading state from status.

  • status is one of 'idle', 'pending', 'success' or 'error'. idle appears with immediate: false or with server: false during the server render.
  • data and error default to undefined in Nuxt 4, not null, so === null checks from Nuxt 3 code never match.
  • With server: false, the fetch does not start until hydration completes, so data is still undefined in <script setup> even if you await it.

A good answer frames this as a product decision: blocking navigation suits pages that are useless without data, and lazy loading suits pages with a usable shell. The docs recommend lazy: true with a loading state for a snappier feel.

11. How do getCachedData, dedupe and the abort signal work together?

getCachedData(key, nuxtApp, ctx) decides whether to reuse existing data instead of running the handler. The default returns payload data during hydration and prerendered static data otherwise.

In Nuxt 4 it is called on every fetch, including watcher triggers and refreshNuxtData, and ctx tells you the cause, so you can cache on navigation but always refetch on a manual refresh.

const { data } = await useFetch('/api/products', {
  getCachedData(key, nuxtApp, ctx) {
    if (ctx.cause === 'refresh:manual') return undefined   // force a fetch
    return nuxtApp.isHydrating
      ? nuxtApp.payload.data[key]
      : nuxtApp.static.data[key]
  },
})

dedupe controls overlapping executions: the default 'cancel' aborts the running request when a new one starts, and 'defer' keeps the running one and skips the new call.

Since 4.2 the handler receives a signal in its second argument, and Nuxt aborts it on a cancelling dedupe, on clear() and when the timeout option expires.

A candidate who passes that signal into $fetch has seen a search box return results out of order.

12. How do you give every API call the same base URL and auth header?

Since Nuxt 4.4, createUseFetch and createUseAsyncData build typed custom composables with your defaults, and Nuxt registers them for key injection so they behave like the built-ins during SSR.

Before that, teams wrapped useFetch by hand and often broke key generation.

// app/composables/useApiFetch.ts
export const useApiFetch = createUseFetch((options) => {
  const config = useRuntimeConfig()
  const token = useCookie('token')   // read in setup context, not inside the hook
  return {
    ...options,
    baseURL: options.baseURL ?? config.public.apiBase,
    onRequest({ options }) {
      if (token.value) options.headers.set('Authorization', `Bearer ${token.value}`)
    },
  }
})

Nuxt 4.5 added a reactive enabled option that blocks every execution while it is false, useful for dependent queries such as "fetch orders once a customer is selected".

Nuxt 4.6 added addons to the factories through defineUseFetchAddon, which covers polling, refresh on focus and retries without forking the composable. A weak answer reaches for an Axios instance and loses payload hydration.

Rendering Modes and Route Rules

13. What rendering modes does Nuxt support, and how do you mix them?

Nuxt defaults to universal rendering: the server renders HTML, the client hydrates it, and later navigations render in the browser.

You can switch the whole app to client-side rendering with ssr: false, prerender it with nuxt generate, or mix modes per route with routeRules, which the rendering docs call hybrid rendering.

export default defineNuxtConfig({
  routeRules: {
    '/': { prerender: true },
    '/products/**': { swr: 3600 },
    '/blog/**': { isr: true },
    '/admin/**': { ssr: false },
    '/api/**': { cors: true },
    '/old-page': { redirect: '/new-page' },
  },
})

Candidates should know that hybrid rendering is not available with nuxt generate, which produces a fully static output, and that Nuxt maps route rules to native platform rules on Netlify and Vercel.

Since 4.6, pages covered only by ssr: false rules are tree-shaken out of the server bundle.

14. What is the difference between swr and isr in route rules?

Both serve a cached response and regenerate it in the background after the TTL. swr caches on the server or a reverse proxy through Nitro's cache layer, which the node-server preset can do for full responses. isr adds the response to the CDN cache on platforms that support it, which the docs list as Netlify and Vercel.

With isr: true, the page stays cached until the next deploy; with swr: true, Nuxt adds a stale-while-revalidate header without a max age.

The design follow-up: a product page with a per-user price or a "signed in as" header cannot be cached as a whole, because the first visitor's HTML is served to everyone.

A strong candidate moves the personal parts into a client-only component or a server: false fetch and caches the shell.

Since 4.3, isr and swr routes also generate _payload.json, so client navigation to them reads the cached payload instead of calling your API.

15. Find the bug: a hydration mismatch on the homepage.

<script setup lang="ts">
const greeting = new Date().getHours() < 12 ? 'Good morning' : 'Good afternoon'
const variant = Math.random() > 0.5 ? 'a' : 'b'
</script>

<template>
  <h1>{{ greeting }}</h1>
  <HeroBanner :variant="variant" />
  <p>Last updated {{ new Date().toLocaleString() }}</p>
</template>

Bug: all three values are computed twice, once on the server and once in the browser, and they differ. The server runs in its own time zone and locale, Math.random() returns a new number, and the timestamp moves.

Vue logs a hydration mismatch and either patches the DOM, causing a flash, or leaves server text in place next to client state.

Fix: compute the A/B variant once and send it through the payload with useState('variant', () => ...), or better, assign it in server middleware and read it from a cookie so it stays stable across visits.

Render times with <NuxtTime> (Nuxt 3.17+), which is built to avoid this mismatch, and wrap the greeting in <ClientOnly> if it must follow the visitor's local clock.

If the page is cached with swr, the server-side greeting is wrong for most visitors anyway.

16. What are server components and islands, and when would you use one?

A component named with the .server.vue suffix always renders on the server. Its JavaScript and its dependencies stay out of the client bundle, and when its props change, Nuxt fetches new HTML for it over the network.

Under the hood it uses <NuxtIsland>, so lazy and a #fallback slot pass through.

Good uses are heavy, static-per-props output: a markdown renderer with a syntax highlighter, a pricing table built from a large library, a chart rendered to SVG. A server component must have a single root element, HTML comments included.

Selective hydration of children with nuxt-client exists but has limits the docs list. A senior candidate weighs the network round trip on every prop change against the bundle saved, and keeps interactive widgets as normal components.

17. How does lazy hydration work, and how is it different from a Lazy component?

Prefixing a component with Lazy splits it into its own chunk and loads it when rendered.

Adding a hydration strategy to a lazy component goes further: the server still renders its HTML, but the client delays hydrating it until a condition is met.

<template>
  <LazyProductReviews hydrate-on-visible />
  <LazyChatWidget hydrate-on-interaction />
  <LazyNewsletterForm hydrate-on-idle />
  <LazyMobileMenu hydrate-on-media-query="(max-width: 768px)" />
  <LazyLegalFooter hydrate-never />
</template>

The other strategies are hydrate-after (a delay in milliseconds) and hydrate-when (a boolean). hydrate-never trees never run client code, and Nuxt 4.6 added serialize: false for useAsyncData plus experimental.stripNeverHydratedData so data fetched inside those trees stays out of the page payload.

A strong candidate ties this to INP and total blocking time on long pages and checks that nothing above the fold waits for interaction.

18. Should you turn on SSR streaming?

Not by default yet. Nuxt 4.5 added experimental.ssrStreaming, which flushes the head, styles and entry scripts immediately and streams the body as Vue renders, improving time to first byte.

It is off by default and marked experimental in the 4.5.0 release notes.

The trade-off a candidate must explain: streaming commits the status code and headers with the first byte, so anything that changes the response after rendering starts cannot reach the client.

A setResponseStatus(404) in a page, or a cookie written in middleware during render, gets dropped.

Nuxt falls back to buffered rendering for routes with redirect, cache, isr, swr, noScripts or ssr: false rules, disables streaming for bots, and logs dropped mutations in development.

A good answer pilots it on content routes with slow data and a fixed 200 status.

Nitro and Server Routes

19. How do server routes map to files?

Files in server/api/ become routes under /api, files in server/routes/ become routes without the prefix, and a method suffix limits a handler to one HTTP method. Dynamic segments use brackets and catch-alls use [...slug].

server/
  api/
    users/
      index.get.ts       -> GET  /api/users
      index.post.ts      -> POST /api/users
      [id].get.ts        -> GET  /api/users/:id
  routes/
    sitemap.xml.ts       -> /sitemap.xml
  middleware/
    log.ts               -> runs on every request
  plugins/
    storage.ts           -> Nitro plugin, runs at startup
  utils/
    db.ts                -> auto-imported server helpers

Per the server directory docs, calling readBody in a GET handler throws a 405, which is why body-reading handlers use the .post.ts suffix.

Since 4.3 a #server alias replaces long relative imports inside server/, with import protection that stops client code from reaching it.

A strong candidate also knows that handlers return values directly: an object becomes JSON, and a thrown createError becomes an error response.

20. Write a validated POST handler that is ready for Nuxt 5.

On Nuxt 4.6, import the handler and helpers from nuxt/server. readValidatedBody accepts any Standard Schema library, such as Zod or Valibot, or a plain function, and the same file runs on Nitro v2 today and Nitro v3 later.

// server/api/projects.post.ts
import { createError, defineEventHandler, readValidatedBody } from 'nuxt/server'
import { z } from 'zod'

const Project = z.object({
  name: z.string().min(1).max(80),
  visibility: z.enum(['private', 'team', 'public']),
})

export default defineEventHandler(async (event) => {
  const input = await readValidatedBody(event, Project)
  const user = event.context.user
  if (!user) {
    throw createError({ status: 401, statusText: 'Unauthorized' })
  }
  const project = await createProject(user.id, input)
  event.res.status = 201
  return project
})

Points to listen for: validation happens before any database call, auth comes from event.context set by server middleware, and errors use status and statusText. Nuxt 4.3 deprecated statusCode and statusMessage in preparation for h3 v2.

The server imports guide lists what nuxt/server covers; for things it does not, such as multipart uploads, the handler keeps importing from h3.

21. Find the bug: a 500 error after adopting nuxt/server.

// server/api/login.post.ts
import { readValidatedBody, useSession } from 'nuxt/server'
import { z } from 'zod'

const Login = z.object({ email: z.string().email(), password: z.string() })

export default defineEventHandler(async (event) => {
  const { email, password } = await readValidatedBody(event, Login)
  const user = await verifyPassword(email, password)
  const session = await useSession<{ userId?: string }>(event)
  await session.update({ userId: user.id })
  return sendRedirect(event, '/dashboard')
})

Bug: defineEventHandler and sendRedirect are not imported, so they resolve to the auto-imported h3 v1 versions. On Nuxt 4.6, the h3 defineEventHandler passes an h3 event, and the nuxt/server helpers expect the portable RequestEvent.

Every request fails with a 500 whose message starts with [NUXT_E8012].

Fix: import defineEventHandler and sendRedirect from nuxt/server in the same file.

The follow-ups worth asking: the nuxt/server sendRedirect returns the response, so it must be returned (this code does that), and its sessions are sealed with a secret derived from NUXT_APP_SECRET and builds never generate one, so production must set it.

Sessions issued by h3's useSession do not unseal with the new helper, so switching logs every user out. Also, verifyPassword should throw a 401 with the same message for unknown email and wrong password.

22. How do you cache a slow upstream call in Nitro?

Use defineCachedEventHandler for a whole route or defineCachedFunction for one function, both from nitropack/runtime on Nuxt 4.

Nitro stores results in its storage layer, keyed by what you choose, and serves stale entries while it revalidates.

// server/api/rates.get.ts
import { getQuery } from 'h3'
import { defineCachedEventHandler } from 'nitropack/runtime'

export default defineCachedEventHandler(async (event) => {
  const { base = 'USD' } = getQuery(event)
  return await $fetch(`https://rates.example.com/latest?base=${base}`)
}, {
  maxAge: 60 * 10,
  getKey: (event) => String(getQuery(event).base ?? 'USD'),
})

A strong candidate raises three risks:

  • The cache key must include everything that changes the output, or one user's response leaks to another.
  • Per-user data should not go through a shared cache at all.
  • On serverless platforms the default in-memory storage lives per instance, so a real cache needs a storage driver such as Redis or the platform's KV mounted in nitro.storage.

They should also know this code is tied to the Nitro v2 runtime; the Nuxt module compatibility docs map it to nitro/cache on Nitro v3.

23. How does Nitro deploy the same app to Node, serverless and edge?

Nitro builds the server into .output/ with a preset that matches the target: node-server by default, and presets for Vercel, Netlify, Cloudflare, AWS Lambda, Deno, Bun and others.

Set it with nitro.preset in config or NITRO_PRESET at build time; several providers are detected automatically. NITRO_PRESET=node_cluster runs multiple processes with the Node cluster module.

The interview value is in the constraints each target adds:

  • Edge runtimes have no Node built-ins such as fs or native modules, so database drivers and image libraries may not run there.
  • Serverless functions lose in-memory state between invocations, which affects caches, rate limiters and WebSocket plans.
  • event.waitUntil(promise) lets a handler finish logging or cache writes after responding, integrated with each platform's native mechanism.
  • Runtime config comes from environment variables at runtime, so the same build can move between staging and production.

24. Design question: an API route needs data from three microservices. How do you build it?

A good answer starts with the contract: one server route in server/api that the page calls through useFetch, so browsers never see internal service URLs or tokens.

Inside, it calls the services in parallel with Promise.all, applies a timeout per call, and decides per service whether a failure fails the route or returns partial data.

  • Secrets come from useRuntimeConfig(), never from runtimeConfig.public.
  • Calls to other routes of the same app use event.$fetch (or serverFetch under nuxt/server), which forwards context without an HTTP hop.
  • Responses with a stable key get a defineCachedFunction wrapper, with a TTL per service.
  • The handler returns only fields the page needs, which keeps the payload small; Nuxt 4.6 warns in development when a page payload passes 100 kB.

Weak answers call all three services from the component with $fetch, leak tokens into the client bundle, or chain the calls one after another.

Modules and Layers

25. What does a Nuxt module do, and how is one structured?

A module is a function that runs at build time, when Nuxt starts in development or builds for production, and changes the app: it adds plugins, components, composables, server handlers, route rules or build plugins.

Modules do not run at request time; the runtime code they add does. The module anatomy docs show the shape:

// modules/analytics/index.ts
import { addPlugin, addServerHandler, createResolver, defineNuxtModule } from '@nuxt/kit'

export default defineNuxtModule({
  meta: {
    name: 'analytics',
    configKey: 'analytics',
    compatibility: { nuxt: '>=4.0.0' },
  },
  defaults: { endpoint: '/api/_track' },
  setup(options, nuxt) {
    const resolver = createResolver(import.meta.url)
    nuxt.options.runtimeConfig.public.analytics = { endpoint: options.endpoint }
    addPlugin(resolver.resolve('./runtime/plugin.client'))
    addServerHandler({
      route: options.endpoint,
      method: 'post',
      handler: resolver.resolve('./runtime/server/track'),
    })
  },
})

defineNuxtModule merges user options from the configKey with defaults, installs the module once, and checks compatibility.

A candidate who has published a module mentions createResolver for paths that work from node_modules, keeping runtime code in a runtime/ folder that is transpiled, and testing with @nuxt/test-utils.

26. How should one module depend on another?

Declare it in moduleDependencies, which replaced the deprecated installModule. Nuxt installs the dependency in the right order, validates a semver range, and merges configuration you supply for it.

export default defineNuxtModule({
  meta: { name: 'my-ui', configKey: 'myUi' },
  moduleDependencies: {
    '@nuxt/image': {
      version: '>=2',
      defaults: { quality: 80 },   // user config still wins
    },
    '@nuxtjs/color-mode': {
      optional: true,              // only configure it if the user installed it
      overrides: { classSuffix: '' },
    },
  },
  setup() {},
})

The distinction to check: defaults sit below the user's nuxt.config, and overrides sit above it. Version checks only apply when the dependency resolves to a package.json, so they do nothing for local modules.

Since 4.6, a module whose server code imports from nuxt/server no longer needs a peer dependency on h3 or nitropack, which is the main reason module authors are adopting it.

27. What are layers, and when do you reach for one instead of a module?

A layer is a partial Nuxt app, with its own nuxt.config, components, composables, pages and server routes, that another app pulls in with extends.

Use a layer to share app code across products, such as a design system, an auth flow or admin pages, and a module to add build-time behavior.

export default defineNuxtConfig({
  extends: ['../shared-layer', 'github:acme/nuxt-base#v2'],
  image: false,   // since 4.3: disable a module the layer registers
})

Things a senior candidate knows: the consuming app overrides files from layers with the same path, named layers get a #layers/<name> alias, and Nuxt 4 corrected the module loading order across layers.

They also know the cost: a layer couples apps to one Nuxt version, so upgrades have to move together.

What Changed Recently

Jul 15, 2025
Nuxt 4.0: app/ directory, singleton data layer, split TypeScript projects
Oct 25, 2025
Nuxt 4.2: abortable useAsyncData, opt-in Vite Environment API, compatibilityVersion: 5
Jan 22, 2026
Nuxt 4.3: ISR and SWR payload extraction, #server alias, Nuxt 3 support extended
Mar 12, 2026
Nuxt 4.4: createUseFetch, vue-router v5, typed layout props
Jul 18, 2026
Nuxt 4.5: Vite 8, Rspack 2, experimental SSR streaming, stable error codes
Jul 31, 2026
Nuxt 3 reaches end of life
Oct 5, 2026
Nuxt 4.6 and Nuxt CLI v4: nuxt/server, sessions, rebuilt typed $fetch
Oct 29, 2026
Nitro v3 enters release candidate

28. What did Nuxt 4.0 change that breaks Nuxt 3 code?

Nuxt 4.0 shipped on July 15, 2025 as a stability-focused major. Most of its breaking changes had been testable behind a compatibility flag for over a year. The ones that show up in interviews:

  • Directory structure: app/ as srcDir, with server/, shared/ and public/ at the root.
  • Singleton data fetching: shared refs per key, cleanup on unmount, reactive keys, and getCachedData called on every fetch with a cause.
  • Shallow data: deep: false by default (question 9).
  • Defaults: data and error start as undefined, not null; boolean values for dedupe on refresh were removed.
  • TypeScript: separate projects for app, server, shared and build code, with one root tsconfig.json. The release notes call this the change most likely to surprise upgraders.
  • Kit: Nuxt 2 compatibility removed from @nuxt/kit, which hits module authors.

The recommended path is npx nuxt upgrade --dedupe, optionally the nuxt/4/migration-recipe codemod, then the upgrade guide section by section.

A candidate who did the migration should name at least the null to undefined change and the shallow data change, because both fail silently.

29. What happened to Nuxt 3 support, and what should a team still on it do?

Nuxt 3 reached end of life on July 31, 2026. The team first announced January 31, 2026, then extended security and critical fixes by six months in the 4.3 release.

The final 3.x patch was 3.21.11 on August 5, 2026, and the 4.6.0 notes confirm there is no 3.x release alongside it.

A team still on Nuxt 3 should move to 4.6, not wait for Nuxt 5: the 4.x line already runs many Nuxt 5 defaults behind flags, so one migration now and a small one later beats two large ones back to back.

For the upgrade itself, the order that works is to set future.compatibilityVersion: 4 on the last 3.x release, fix what breaks, then bump the version.

30. What is in Nuxt 4.6 that a senior candidate should know?

Nuxt 4.6.0 (October 5, 2026) had over 420 commits since 4.5.2. The parts that change how you write code:

  • nuxt/server: web-standard server helpers typed against a portable RequestEvent (event.req, event.url, event.res). Backed by Nitro and h3 by default, with an experimental @nuxt/vite-server alternative built on the Vite Environment API.
  • appSecret and sessions: one NUXT_APP_SECRET, deriveSecret(purpose) for modules, and cookie-sealed sessions with no server storage.
  • Typed $fetch rebuilt: behind experimental.routeTypedFetch, with body, query and header types checked from the validators in your handlers.
  • Vue Vapor: opt-in per component with <script setup vapor>, requiring Vue 3.6.0-rc.2 or newer.
  • Performance: server-rendered <NuxtLink> 58% faster and experimental.early404 for requests that match no page.
  • Nuxt CLI v4: an interactive nuxt dev UI, requiring Node.js 22.21+, 24.11+ or 26+; nuxt init is dropped in favor of npm create nuxt@latest.

The current patch is 4.6.4 (November 2, 2026). A candidate who mentions the Node.js floor has probably upgraded a CI image this quarter.

31. Has Nuxt 5 shipped, and how do you prepare for it?

No. As of November 2026, Nuxt 5 is in development and the upgrade guide describes it as such.

Its main dependency, Nitro v3 with h3 v2, published its first release candidate on October 29, 2026 (Nitro releases), and Nuxt 4.6 still depends on nitropack 2.13.

Preparation happens on 4.6 with future.compatibilityVersion: 5, which turns on the Nuxt 5 defaults that can run on Nuxt 4. Among them:

  • The Vite Environment API, case-sensitive routing and typed pages.
  • Client-side payload extraction and serializable page meta extraction.
  • Typed $fetch from route types, and Nitro auto-imports off, so server code imports helpers explicitly.
  • The Options API compiled out of the client bundle, and PostCSS autoprefixer and cssnano no longer configured by default.

Each default can be toggled on its own. The flag does not switch Nitro to v3; that only comes with Nuxt 5.

The practical step is to move server handlers to explicit nuxt/server imports now and keep h3-specific code, such as multipart parsing or Nitro caching, in a small, labeled set of files.

Signs of a Strong Answer

  • They say where each line of a page runs (server, client or both) before explaining what it does.
  • They use useFetch or useAsyncData for render data and plain $fetch only in event handlers, and can explain the double fetch otherwise.
  • They know Nuxt 4 data is shallow and defaults to undefined, and spot both in old code.
  • They pick a route rule per page and say what must stay out of a cached response.
  • They write server handlers with explicit nuxt/server imports and validated input, and know which helpers still tie them to Nitro v2.
  • They state the current versions accurately: Nuxt 4.6, Nuxt 3 past end of life, Nitro v3 still a release candidate.

Hiring Nuxt Developers

Nuxt developers who have migrated an app from Nuxt 3 to 4, run hybrid rendering in production and written their own server routes are harder to find than general Vue developers.

Second Talent helps US startups hire vetted AI-Native Engineers from Asia, including Nuxt developers and broader Vue developers, screened with questions like these.

For the Vue side of the loop, use our Vue 3 and TypeScript interview guides.

Rates run $23 to $39 USD an hour, billed monthly, with matches in 24 hours and a 90-day one-time replacement guarantee. Compare budgets on the cost to hire a Vue developer page, or tell us what you need.

Get developers who ship with AI agents.

Starts at $23/Hour.

Start Hiring

$0 cost until you hire