Vue 3 and Nuxt rules for AI coding agents
Vue's reactivity is forgiving until it is not: destructure a reactive object and the values stop updating, keep state at module level in a Nuxt composable and every visitor's server render shares it. These rules cover Vue 3 with the Composition API and Nuxt for server rendering, and the agent first checks vue and nuxt in package.json, since Nuxt 4 moves app code under app/ by default.
They ask for <script setup> components without mixing in the Options API, ref by default, stable :key values in v-for, Pinia stores per domain, and useFetch or useAsyncData for page data so nothing fetches twice during hydration. Server routes run in Nitro, not Vue, so composables stay out of server/ code.
Folder: rules/vue-nuxt/ · vue-nuxt.mdc, CLAUDE.md, AGENTS.md
What the Vue and Nuxt rules tell your agent
- ComponentsWrite single-file components with
<script setup lang="ts">when the project uses TypeScript, and<script setup>otherwise. Do not mix the Options API into a Composition API codebase. - ReactivityUse
refby default; it works for primitives and objects and survives reassignment. Usereactiveonly for objects you never replace. - TemplatesEvery
v-forgets a stable:keyfrom the data, not the index, when the list can change order. - StateUse Pinia for shared client state, one store per domain (
useCartStore). Keep server data in fetch composables rather than copying it into stores. - Nuxt data fetchingFetch page data with
useFetchoruseAsyncDatainsetup. They run on the server, transfer the payload, and do not refetch on hydration. Calling$fetchdirectly insetupfetches twice (server and client).
First rule of each section in vue-nuxt.mdc, quoted as shipped.
Mistakes these rules are written to prevent
- Hydration mismatches from dates, random ids or
localStoragereads during render. UseuseId()for ids and read browser storage after mount. - Using Vue composables (
useFetch,useState,ref) inserver/code. Server routes run in Nitro, not Vue; use$fetch,useRuntimeConfig(event)and h3 helpers there. - Using
thisinside<script setup>; there is no component instance there.
Source: “Pitfalls to avoid”.
Checks the agent runs before it says done
Example commands from the Vue and Nuxt file (swap in your repo's own):
- Type check:
npx nuxi typecheck(ornpx vue-tsc --noEmit) - Lint:
npm run lint - Tests:
npx vitest run - Build:
npx nuxi build
Run vue-tsc --noEmit (or nuxi typecheck), the ESLint command, the unit tests, and nuxi build when you touched server routes or rendering mode. Report each result.
Before and after: Vue and Nuxt: lost reactivity and cross-request state
Prompt card 39 · Stack: Vue 3 Composition API, Nuxt 3/4 · Type: Review and refactor · Best model: a frontier reasoning model
What the card is for
Find values that stopped being reactive (destructured reactive objects or stores, props copied into plain variables), watchers on the wrong source, and module-level state that Nuxt shares across all users on the server.
Worked example from the card
Before: composables/useCart.ts and a component
const cart = reactive({ items: [] as CartItem[], coupon: null as string | null });
export function useCart() {
const add = (item: CartItem) => cart.items.push(item);
return { ...cart, add };
}
<script setup lang="ts">
const { items, coupon, add } = useCart()
const total = items.reduce((s, i) => s + i.priceCents * i.qty, 0)
</script>
<template><p>{{ items.length }} items, {{ total }} cents</p></template>
Expected AI output (abridged)
1. Module-level `cart` in a Nuxt composable: created once per server process, so every
visitor's server render shares one cart. User A's items appear in user B's HTML.
2. `{ ...cart }` spreads the reactive object into plain properties. `items` is the same
array (so push still mutates it), but `coupon` is a snapshot string that never updates.
3. `total` is computed once at setup with reduce; it never changes when items are added.
Fix: useState for SSR-safe per-request state, computed for total, return refs.
After
export function useCart() {
const cart = useState("cart", () => ({ items: [] as CartItem[], coupon: null as string | null }));
const items = computed(() => cart.value.items);
const coupon = computed(() => cart.value.coupon);
const totalCents = computed(() => cart.value.items.reduce((s, i) => s + i.priceCents * i.qty, 0));
function add(item: CartItem) { cart.value.items.push(item); }
return { items, coupon, totalCents, add };
}
<script setup lang="ts">
const { items, totalCents } = useCart()
</script>
<template><p>{{ items.length }} items, {{ totalCents }} cents</p></template>
Excerpt from card 39; the prompt itself is in the pack.
Using these rules in Cursor, Claude Code and AGENTS.md tools
Cursor
Put _base.mdc and vue-nuxt.mdc in .cursor/rules/. Globs: nuxt.config.*, **/*.vue, composables/**/*.ts, server/**/*.ts, stores/**/*.ts, app/**/*.ts.
Claude Code
Root CLAUDE.md, base first. Path-scoped variant .claude/rules/vue-nuxt.md for **/*.vue, server/**/*.ts, composables/**/*.ts; PostToolUse hook: npx prettier --write <file>.
AGENTS.md tools (Codex, Gemini CLI, Aider)
Base then stack in one AGENTS.md. Gemini CLI needs context.fileName; Aider needs --read AGENTS.md. Put this file in apps/web/ or the repo root. Agents use the AGENTS.md nearest to the file being edited, so a nested copy overrides a root one for that subtree.