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.
# 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`)