Cursor Rules for Vue 3 and Nuxt: A Setup Guide
Set up Cursor rules for Vue 3 and Nuxt that stop Options API relapse, settle ref vs reactive, and sync to Claude Code, Copilot, and Windsurf.
AI coding tools write Vue like it's 2021. Ask Cursor or Claude Code for a component in an unconfigured project and there's a real chance you get Options API code, a reactive() object destructured into dead variables, or an onMounted fetch that silently breaks SSR in your Nuxt app. Cursor rules for Vue fix this: a short, explicit set of instructions that pins the AI to <script setup>, settles the ref vs reactive question once, and teaches it your Nuxt data-fetching patterns.
This guide gives you that rule set. We'll cover the mistakes AI tools make most often in Vue 3 and Nuxt codebases, the exact rules that prevent each one, and the step-by-step setup for Cursor, Claude Code, GitHub Copilot, and Windsurf so every tool behaves the same way. If you're new to rules files in general, start with the complete guide to Cursor rules — this post is the Vue-specific layer on top.
Why you need cursor rules for Vue 3
Vue 3 AI coding fails in predictable ways. Models are trained on a decade of Vue content, and most of that decade was Vue 2. Without rules, they average across eras and styles, and you get code that's syntactically valid but wrong for any modern codebase.
Options API relapse
The single most common failure: you ask for a component and get export default { data() { ... }, methods: { ... } }. It compiles, it even works, and now your codebase has two component styles. This is the Vue equivalent of getting Pages Router code in a Next.js App Router project — the same failure mode we covered in AI coding rules for React and Next.js, with the same fix: an explicit, non-negotiable rule.
ref vs reactive confusion
Even when the AI uses the Composition API, it mixes ref() and reactive() arbitrarily — and regularly generates the classic reactivity-loss bug:
// Loses reactivity — a classic AI-generated bug
const state = reactive({ count: 0, name: "" })
const { count } = state // count is now a dead, non-reactive number
// What your rules should enforce instead
const count = ref(0)
const name = ref("")
Related misses: forgetting .value in script code, adding .value in templates where it doesn't belong, and using a watch to sync state that should be a computed.
Nuxt-specific drift
In Nuxt projects the failure surface doubles. AI tools add manual imports for things Nuxt auto-imports, fetch initial data with raw fetch in onMounted (killing SSR), call $fetch during setup (double-fetching on hydration), and write Express-style handlers instead of h3 event handlers in server/api/. Each of these gets its own rule below.
Composition API rules that stick
The rules that work are specific, prescriptive, and explain the why in one line so the model can generalize. Here's the core Vue 3 block:
## Vue conventions
- Composition API with <script setup lang="ts"> exclusively.
- Never generate Options API code: no data(), methods:, computed: {},
watch: {}, or object-style lifecycle hooks.
- Use ref() for all local state, including objects. Use reactive() only
for grouped state that is never destructured or reassigned.
- Never destructure a reactive() object — it severs reactivity.
Use toRefs() if destructuring is unavoidable.
- Access refs with .value in script. Never write .value in templates
(refs auto-unwrap there).
- Use computed() for derived state. Never use a watcher to keep one
piece of state in sync with another.
- Prefer watch() with explicit sources over watchEffect(). Reserve
watchEffect() for fire-and-forget side effects.
- Extract reusable logic into composables named useThing() that return
an object of refs, so callers can destructure safely.
Two design notes. First, "use ref() for everything" is deliberately opinionated — the point of a rule is to end the debate, not to document both options. If your team prefers reactive() for form state, write that instead; what matters is that the rule picks one. Second, the "never destructure reactive()" line earns its place even if you barely use reactive(), because AI tools generate that exact bug constantly when summarizing state into local variables.
Nuxt cursor rules: auto-imports, server routes, data fetching
Nuxt cursor rules deserve their own section in your rules file because Nuxt's conventions are invisible in the code itself — auto-imports mean there's no import statement for the model to pattern-match on.
Auto-imports
## Nuxt auto-imports
- Rely on auto-imports. Do NOT add manual imports for ref, computed,
watch, useFetch, useAsyncData, useRoute, useRouter, navigateTo,
or anything under composables/ or utils/.
- Components in components/ are auto-imported by path-based name.
Do not import them manually in other components.
Redundant imports won't break anything, but they create diff noise and — worse — teach the next generation round that manual imports are the house style.
Data fetching
This is where unguided AI does real damage, because the broken version still renders:
## Nuxt data fetching
- Use useFetch() for component data. It is SSR-aware, deduplicated,
and transfers state to the client without refetching.
- Use useAsyncData() + $fetch when you need a custom key, transform,
or non-HTTP data source.
- Use bare $fetch ONLY inside event handlers and server code.
Never call $fetch at the top level of setup — it fetches twice
(server and client).
- Never fetch initial page data with raw fetch() in onMounted —
it skips SSR entirely.
The correct pattern, for reference:
<script setup lang="ts">
const route = useRoute()
const { data: product, error } = await useFetch(
() => `/api/products/${route.params.id}`
)
</script>
Server routes
Nuxt's server engine is h3, and models frequently reach for Express idioms instead. Pin the pattern with a concrete example:
## Nuxt server routes
- Server routes live in server/api/, named by route and method:
server/api/products/[id].get.ts
- Always use defineEventHandler. Read input with readBody(event),
getQuery(event), and getRouterParam(event, "id").
- Throw errors with createError({ statusCode, statusMessage }).
Never return raw error strings or res.status() calls.
- Read config with useRuntimeConfig(event), not process.env.
// server/api/products/[id].get.ts
export default defineEventHandler(async (event) => {
const id = getRouterParam(event, "id")
const product = await findProduct(id)
if (!product) {
throw createError({ statusCode: 404, statusMessage: "Product not found" })
}
return product
})
Setting up cursor rules for Vue across every tool
Same rules, four file formats. Here's the setup, step by step.
Step 1: Cursor — .cursor/rules/vue.mdc
Cursor's current format is .mdc files under .cursor/rules/, each with frontmatter controlling when it applies:
---
description: Vue 3 Composition API and Nuxt conventions
globs: ["**/*.vue", "composables/**", "server/**"]
alwaysApply: false
---
## Vue conventions
- Composition API with <script setup lang="ts"> exclusively.
...
Scoping with globs keeps the rules out of context when you're editing unrelated files. A legacy Vue .cursorrules file at the repo root still works, but it's a single unscoped blob — treat it as read-only history and migrate to .mdc. There are plenty of real-world layouts to borrow from in these cursorrules examples.
Step 2: Claude Code — CLAUDE.md
Your Vue Claude Code rules live in CLAUDE.md at the repo root. There's no frontmatter and no glob scoping — the file loads with every session — so keep it tighter than your Cursor rules and lead with the highest-value constraints (<script setup> only, the ref/reactive policy, the useFetch hierarchy). Put long code examples in Cursor's scoped rules rather than here.
Step 3: GitHub Copilot — .github/copilot-instructions.md
Copilot reads .github/copilot-instructions.md. The same markdown body works verbatim; drop the .mdc frontmatter.
Step 4: Windsurf — .windsurf/rules/
Windsurf reads rules files from .windsurf/rules/. Again, same content, different location.
That's four copies of one document. They will drift — someone updates the data-fetching rule in Cursor after a code review and never touches the other three. We'll fix that in the last section.
Component and testing conventions
Beyond the framework mechanics, AI output drifts on the conventions that make a Vue codebase feel coherent:
## Components
- Multi-word PascalCase component names (UserCard, not Card) to avoid
colliding with HTML elements.
- Type-based props and emits:
defineProps<{ user: User; compact?: boolean }>()
defineEmits<{ select: [id: string] }>()
Use withDefaults() for default values.
- SFC block order: <script setup>, then <template>, then <style scoped>.
- Never combine v-if and v-for on the same element.
- Always key v-for with a stable id, never the array index.
## Testing
- Vitest. Test files sit next to source: user-card.spec.ts.
- Use Vue Test Utils mount() for plain components. For anything that
touches Nuxt auto-imports or plugins, use mountSuspended from
@nuxt/test-utils/runtime.
- Mock server routes with registerEndpoint from @nuxt/test-utils,
not global fetch stubs.
- Assert on rendered output and emitted events, not wrapper.vm
internals.
The Nuxt testing rules matter more than they look: without them, AI-generated tests mount() a component that depends on useFetch, watch it explode, and then "fix" it by mocking half the framework by hand.
The full copyable rule set
Everything above, assembled into one file. Adapt the stack section, then use it as your .mdc body, CLAUDE.md section, and Copilot instructions:
# Project: [Your App]
## Stack
- Vue 3.5+, Composition API, <script setup lang="ts"> only
- Nuxt 4 (app/ directory), TypeScript strict
- Pinia for shared state
- Vitest + Vue Test Utils + @nuxt/test-utils
## Vue
- Never generate Options API code (no data(), methods:, computed: {},
watch: {}, object lifecycle hooks)
- ref() for all local state; reactive() only for grouped state that is
never destructured or reassigned
- Never destructure reactive() — use toRefs() if unavoidable
- .value in script, never in templates
- computed() for derived state; never sync state with watchers
- watch() with explicit sources; watchEffect() for fire-and-forget only
- Composables in composables/, named useThing(), returning objects of refs
## Nuxt
- Rely on auto-imports — no manual imports for Vue reactivity APIs,
Nuxt composables, or anything in composables/ and utils/
- useFetch() for component data; useAsyncData() + $fetch for custom
keys or transforms
- $fetch only in event handlers and server code, never top-level setup
- Never fetch initial data with raw fetch() in onMounted
- Server routes in server/api/ with defineEventHandler; use readBody,
getQuery, getRouterParam; errors via createError()
- useRuntimeConfig() instead of process.env in app code
## Components
- Multi-word PascalCase names
- Type-based defineProps / defineEmits; withDefaults() for defaults
- Block order: script setup, template, style scoped
- No v-if with v-for on the same element; stable keys for v-for
## Testing
- Vitest; specs next to source files
- mountSuspended from @nuxt/test-utils/runtime for Nuxt-aware components
- registerEndpoint for API mocking
- Assert rendered output and emitted events, not component internals
One rule set, not four drifting copies
The four-file problem from Step 4 is what localskills.sh exists to solve. You publish this rule set once as a versioned skill, and one install writes every tool-native format — .cursor/rules/*.mdc for Cursor, .claude/skills/ for Claude Code, .windsurf/rules/ for Windsurf:
localskills install your-team/vue-nuxt-rules --target cursor claude windsurf
When you tighten the data-fetching rule after the next incident, you publish a new version and the whole team pulls it — no stale copies, and rollback if a change misfires. Skills are folder packages under a root SKILL.md, so the Vue rules, the Nuxt server-route examples, and a component template can ship together as one unit.
Ready to stop re-teaching every AI tool your Vue conventions one file at a time? Create a free account, publish your Vue and Nuxt rule set, and install it everywhere with one command.
npm install -g @localskills/cli
localskills login
localskills publish