Vue 3.5 & Nuxt 4.4 logo

Vue 3.5 & Nuxt 4.4

0

Vue 3.5.34 + Nuxt 4.4.5 rules for Cursor. Pinned to vue ^3.5.34, nuxt ^4.4.5, pinia ^3.0, vue-router ^5.0, @vueuse/core ^14.3, @nuxt/ui ^4.7, vitest ^4.1. Leads with the Vue 3.5 trio LLMs miss (useTemplateRef, useId, reactive props destructure with defaults), the Nuxt 4 app/ srcDir layout (with server/, shared/, public/ at root), useAsyncData shallowRef + singleton-by-key behaviour, Pinia 3 setup stores, VueUse foundational composables, and Vitest 4 + Vue Test Utils 2 + @nuxt/test-utils. Catches 38 LLM regressions including Options API in new components, ref(null) template refs, withDefaults wrapping, Vuex createStore, module-scope ref leaks in Nuxt composables, readBody in GET handlers, missing method-suffix on server routes, store/ instead of stores/, compatibilityVersion: 3 in Nuxt 4, queryContent in @nuxt/content v3, manual addEventListener over useEventListener, eager hydration of below-the-fold async components.

10 rules

# Nuxt 4 Core Cursor: Nuxt 4 (GA 2025-07-15) flipped the default `srcDir` to `app/` and split the TypeScript project so client, server, and shared code each get their own type context. Target: `nuxt ^4.4.5` on Node 20+ (22 recommended), with `nitropack ^2.13.4` (stable) and `h3 ^1.15.11` (stable). DO NOT teach Nitro v3 or h3 v2; both are pre-release as of this rule and the public API has not stabilised. The companion `vue-nuxt-anti-patterns` rule rejects the Nuxt 3 layout in a Nuxt 4 project (entries #25, #26). ## Skeleton ``` my-nuxt-app/ app/ # srcDir (Nuxt 4 default) app.vue # root component (was at root in Nuxt 3) error.vue # error page pages/ index.vue posts/ [slug].vue components/ PostCard.vue composables/ usePosts.ts layouts/ default.vue middleware/ auth.ts plugins/ analytics.client.ts stores/ # Nuxt 3: stores/ at root. Nuxt 4: app/stores/. This is canonical. posts.ts assets/ css/main.css server/ # NOT under app/ api/ posts/ index.get.ts index.post.ts [id].get.ts middleware/ log.ts plugins/ db.ts routes/ sitemap.xml.get.ts utils/ db.ts shared/ # NOT under app/, NOT under server/ types.ts # types referenced from BOTH client and server constants.ts public/ # static assets served as-is at / favicon.ico robots.txt modules/ # local Nuxt modules my-module/ nuxt.config.ts # at root tsconfig.json # at root, references the three split projects package.json ``` The four directories that stay at the root in Nuxt 4 (NOT under `app/`): - `server/` - server routes, middleware, plugins, utils - `shared/` - code reachable from BOTH client and server - `public/` - static files served verbatim - `modules/` - local Nuxt modules Application UI code (the rest) lives under `app/`. ## `nuxt.config.ts` ```ts import { defineNuxtConfig } from 'nuxt/config' export default defineNuxtConfig({ compatibilityDate: '2025-10-01', modules: [ '@nuxt/ui', '@nuxt/image', '@nuxt/content', '@nuxt/eslint', '@pinia/nuxt', '@vueuse/nuxt', ], // No `future: { compatibilityVersion: 3 }` - the flag was removed in Nuxt 4 // because the new behaviour IS the default. (anti-pattern #26) runtimeConfig: { // server-only: apiSecret: '', public: { // exposed to client: apiBase: '/api', }, }, app: { head: { htmlAttrs: { lang: 'en' }, meta: [{ name: 'viewport', content: 'width=device-width, initial-scale=1' }], }, }, typescript: { strict: true, typeCheck: true, }, experimental: { typedPages: true, // typed router via .nuxt/types/typed-router.d.ts }, }) ``` ## `compatibilityDate` (not `compatibilityVersion`) Two different flags: - `compatibilityDate: '2025-10-01'` (Nitro feature) - opts into runtime fixes shipped after that date. Set to a recent date for new projects. Required by Nitro 2.13+. - `future: { compatibilityVersion: 3 }` (Nuxt 3.12 only) - REMOVED in Nuxt 4. Setting it now is at best a no-op, at worst flagged by the config schema. (anti-pattern #26) ## `tsconfig.json` references the three split projects Nuxt 4 generates three separate TypeScript projects so Vue runtime types do not pollute server code (and Nitro types do not pollute client code). ```json { "files": [], "references": [ { "path": "./.nuxt/tsconfig.app.json" }, { "path": "./.nuxt/tsconfig.server.json" }, { "path": "./.nuxt/tsconfig.shared.json" } ] } ``` `vue-tsc` typechecks against all three. Editor language services pick the right one based on file location (`app/**/*` -> app, `server/**/*` -> server, `shared/**/*` -> shared). ## Auto-imports (client side) Inside files under `app/` Nuxt auto-imports: - All Vue reactivity (`ref`, `computed`, `watch`, `reactive`, `shallowRef`, `toRaw`, etc.) - Vue lifecycle (`onMounted`, `onBeforeUnmount`, etc.) - Vue 3.5 additions (`useTemplateRef`, `useId`, `onWatcherCleanup`) - Nuxt composables (`useFetch`, `useAsyncData`, `useState`, `useRoute`, `useRouter`, `useNuxtApp`, `useRequestEvent`, `useRequestHeaders`, `useHead`, `useSeoMeta`, `useCookie`, etc.) - Anything you export from `app/composables/` - Anything you export from `app/utils/` - All `app/components/*.vue` as components in templates So: ```vue <script setup lang="ts"> // No imports needed for these inside a Nuxt SFC under app/: const route = useRoute() const id = computed(() => route.params.id as string) const { data } = await useFetch(`/api/posts/${id.value}`) </script> ``` For library imports (`@vueuse/core`, `zod`, anything else) you DO write the import. ## Auto-imports (server side) Server files (`server/**/*.ts`) auto-import: - h3 helpers (`defineEventHandler`, `getQuery`, `readBody`, `readValidatedBody`, `getValidatedQuery`, `createError`, `setResponseStatus`, etc.) - Anything you export from `server/utils/` - Nuxt server composables (`useRuntimeConfig`) Server files do NOT auto-import Vue runtime, Nuxt client composables, or Pinia. Those have no meaning on the server. ## `shared/` for types reachable from both sides Use `shared/` for type definitions referenced from client AND server. The split tsconfigs both reference it. ```ts // shared/types.ts export interface Post { id: string title: string slug: string publishedAt: string } ``` ```ts // app/composables/usePost.ts import type { Post } from '~~/shared/types' export const usePost = (id: string) => useFetch<Post>(`/api/posts/${id}`) ``` ```ts // server/api/posts/[id].get.ts import type { Post } from '~~/shared/types' export default defineEventHandler(async (event): Promise<Post> => { const id = getRouterParam(event, 'id')! // ... }) ``` The `~~` prefix is the project-root alias; `~` resolves to `srcDir` (`app/` in Nuxt 4). ## `runtimeConfig` over `process.env` `runtimeConfig` is read once at boot, exposes the `public` block to the client, and surfaces in `useRuntimeConfig()`. Reading `process.env.FOO` inside a request handler is unreliable in some deploy targets. ```ts // nuxt.config.ts runtimeConfig: { apiSecret: '', // server-only; reads NUXT_API_SECRET at boot public: { apiBase: '/api', // client-readable; reads NUXT_PUBLIC_API_BASE }, }, ``` ```ts // server/api/things.get.ts export default defineEventHandler(() => { const config = useRuntimeConfig() return $fetch(`${config.public.apiBase}/things`, { headers: { Authorization: `Bearer ${config.apiSecret}` }, }) }) ``` ## `modules` array Common modules for a Nuxt 4 + Vue 3.5 project: | Module | Why | |--------|-----| | `@pinia/nuxt` | Pinia 3 setup-store integration with auto-imported `defineStore`, `storeToRefs` | | `@vueuse/nuxt` | Auto-imports VueUse composables (`useEventListener`, `onClickOutside`, `useDebounceFn`) | | `@nuxt/ui` | UI library v4 on Tailwind v4 + Reka UI | | `@nuxt/image` | Optimised `<NuxtImg>` / `<NuxtPicture>` | | `@nuxt/content` | SQL-backed content collections (`queryCollection`, NOT `queryContent`) | | `@nuxt/eslint` | Flat-config ESLint preset | ## What changed from Nuxt 3 to Nuxt 4 | Change | Nuxt 3 | Nuxt 4 | |--------|--------|--------| | Default `srcDir` | project root | `app/` | | `app.vue` location | `/app.vue` | `app/app.vue` | | `pages/`, `components/`, `composables/` | at root | under `app/` | | `server/`, `shared/`, `public/`, `modules/` | at root | at root (unchanged) | | `useAsyncData` / `useFetch` data | `ref` (deep) | `shallowRef` (configurable via `deep: true`) | | Same-key `useFetch` | each call refetched | singleton-by-key (shared payload) | | `compatibilityVersion` flag | preview opt-in | REMOVED (always-on) | | TypeScript projects | one | three (`.app`, `.server`, `.shared`) | | Reactive scope cleanup | manual | better default lifetimes | ## What never appears in a Nuxt 4 project - `pages/` / `components/` / `composables/` at the root (anti-pattern #25; must be under `app/`) - `future: { compatibilityVersion: 3 }` in `nuxt.config.ts` (anti-pattern #26; flag removed) - `store/` directory (anti-pattern #27; was already `stores/` in Nuxt 3, now `app/stores/`) - Manual `import { ref } from 'vue'` inside an SFC under `app/` (anti-pattern #14; auto-imported) - `process.env.FOO` inside a request handler (use `useRuntimeConfig()`) ## What to refuse to generate - A `nuxt.config.ts` with `srcDir: '.'` for a fresh project (it forces Nuxt 3 layout for no reason) - A new project with Nitro v3 or h3 v2 pinned (both pre-release; pin Nitro 2.13 stable + h3 1.15 stable) - `pages/index.vue` at the project root in a Nuxt 4 project (move to `app/pages/index.vue`)