Vue 3 + Nuxt AI Rules

Rules for Nuxt 3 projects with Vue 3 composition API: <script setup>, auto-imports, Pinia state, server routes, and useFetch / useAsyncData patterns. No Options API holdovers.

TypeScriptNuxt#vue#nuxt#composition-api#piniaLast updated 2026-05-05
tune

Want to customize this rules file? Open the generator with this stack pre-loaded.

Open in generatorarrow_forward

Save at .cursor/rules/main.mdc

Vue 3 + Nuxt 3

Project context

This is a Nuxt 3 project using Vue 3 with the Composition API and <script setup>. We rely on Nuxt's auto-imports for ref, computed, useFetch, etc. State management with Pinia.

Stack

  • Nuxt 3+
  • Vue 3 (Composition API + <script setup> only — no Options API)
  • TypeScript strict mode
  • Tailwind CSS 4 via @nuxtjs/tailwindcss
  • Pinia for global state
  • VueUse for utility composables
  • Vitest + Vue Test Utils
  • pnpm

Folder structure (Nuxt conventions)

.
├── app.vue                  — root component (replaces _app.tsx)
├── nuxt.config.ts
├── pages/                   — file-based routes
│   ├── index.vue
│   ├── blog/[slug].vue
├── components/              — auto-imported when used in templates
├── composables/             — auto-imported (useFoo)
├── server/
│   ├── api/
│   │   ├── posts.get.ts     — GET /api/posts
│   │   └── posts.post.ts    — POST /api/posts
│   └── routes/              — non-/api routes
├── stores/                  — Pinia stores, auto-imported (defineStore)
├── middleware/              — route middleware
├── layouts/
│   └── default.vue
├── plugins/                 — runtime plugins
├── public/                  — served as-is
├── assets/                  — processed by Vite
└── types/

Auto-imports (don't fight them)

Nuxt auto-imports ref, computed, watch, useFetch, useAsyncData, useRoute, useRouter, useState, useNuxtApp, all components in components/, all composables in composables/, all stores in stores/. Never manually import these.

<script setup> everywhere

<script setup lang="ts">
const props = defineProps<{ title: string; count?: number }>()
const emit = defineEmits<{ change: [value: number] }>()

const local = ref(props.count ?? 0)
const doubled = computed(() => local.value * 2)
</script>

<template>
  <div>
    <h2>{{ title }}</h2>
    <button @click="local++">Count: {{ local }} (×2 = {{ doubled }})</button>
  </div>
</template>
  • Always lang="ts"
  • Type props with defineProps<{...}>() — not the runtime declaration
  • Type emits with defineEmits<{...}>()
  • Refs: ref() for primitives, reactive() for plain objects (and .value on refs in script, no .value in template)

Data fetching

  • useFetch(url) — for HTTP fetches (Nuxt-aware caching, dedupes between server/client)
  • useAsyncData('key', () => fetchSomething()) — for arbitrary async logic
  • $fetch(url) — imperative fetch (use inside event handlers, not in setup)
  • Never raw fetch()useFetch and $fetch integrate with Nuxt's payload hydration
<script setup lang="ts">
const { data: posts, pending, error } = await useFetch('/api/posts')
</script>
  • await useFetch only inside <script setup> (not inside if / loops); for conditionals use useLazyFetch or useAsyncData

Server routes (server/api/)

  • File-based: server/api/posts.get.tsGET /api/posts
  • Use defineEventHandler and getQuery, readBody, createError from h3 (auto-imported)
  • Validate inputs with Zod / valibot at the boundary
  • Use setResponseStatus(event, 201) for non-200 success
export default defineEventHandler(async (event) => {
  const body = await readValidatedBody(event, schema.parse)
  const post = await db.posts.create({ data: body })
  setResponseStatus(event, 201)
  return post
})

Pinia stores

// stores/posts.ts
export const usePostsStore = defineStore('posts', () => {
  const items = ref<Post[]>([])
  const fetched = ref(false)

  async function fetchAll() {
    if (fetched.value) return
    items.value = await $fetch('/api/posts')
    fetched.value = true
  }

  return { items, fetched, fetchAll }
})
  • Setup-style stores (composition API) — not options-style
  • One store per resource / domain
  • Don't put one-off ephemeral state in stores; use useState (Nuxt's SSR-aware shared state)

Patterns to follow

  • Use Nuxt layouts (layouts/default.vue) — don't roll your own
  • Use definePageMeta({ middleware: 'auth' }) for route guards
  • Use useHead({ title, meta }) for SEO instead of manual <head> manipulation
  • For SSR-bound shared state, useState('key', () => initial) — survives hydration

Patterns to avoid

  • Options API — composition only
  • Manual import { ref } from 'vue' — Nuxt auto-imports
  • fetch() directly — use useFetch / $fetch
  • Mutation in computedcomputed is read-only by default
  • Calling $fetch in setup() top-level for SSR data — use useFetch
  • Vuex — use Pinia

Testing

  • Vitest + @vue/test-utils
  • Use mount for components; assert on rendered DOM
  • For composables, test in isolation — they're plain functions

Tooling

  • pnpm dev — dev server with HMR
  • pnpm build — production build
  • pnpm preview — preview built output
  • pnpm test — Vitest
  • pnpm typechecknuxt typecheck

AI behavioral rules

  • Use <script setup lang="ts"> only — never Options API, never plain <script>
  • Type props/emits via the type-only defineProps<{...}>() / defineEmits<{...}>()
  • For data fetching in pages, use useFetch or useAsyncData — never raw fetch
  • Don't manually import auto-imported APIs (ref, computed, useFetch)
  • Server routes go under server/api/ with file-name HTTP method suffix
  • Use Pinia setup-stores; never Vuex
  • Run nuxt typecheck and vitest before declaring a task done

Frequently asked

How do I use this Nuxt rules file with Cursor?

Pick "Cursor (.cursor/rules/*.mdc)" from the format dropdown above and click Copy. Save it at .cursor/rules/main.mdc in your project root and restart Cursor. The legacy .cursorrules format still works if you're on an older Cursor version — pick that option instead.

Can I use this with Claude Code (CLAUDE.md)?

Yes — pick "Claude Code (CLAUDE.md)" from the format dropdown above and copy. Save the file as CLAUDE.md at your repo root. Claude Code reads it automatically on every session. For monorepos, you can also drop nested CLAUDE.md files in subdirectories — Claude merges them when working in those paths.

Where exactly do I put this file?

It depends on the AI tool. Cursor reads .cursorrules or .cursor/rules/*.mdc at the project root. Claude reads CLAUDE.md at the project root. Copilot reads .github/copilot-instructions.md. The "Save at" path under each format in the dropdown shows the exact location for the format you picked.

Can I customize these Nuxt rules for my project?

Yes — that's what the generator is for. Click "Open in generator" above and the wizard loads with this stack's defaults pre-selected. Toggle on or off the conventions you want, then re-export in your AI tool's format.

Will using this rules file slow down my AI tool?

No. Rules files count toward the model's context window but not toward latency in any noticeable way. The file is loaded once per session, not per token. The library files target 250–400 lines, well within every tool's recommended budget.

Should I commit this file to git?

Yes. The rules file is project documentation that benefits every developer using the AI tool. Commit it. The exception is personal-global settings (e.g. ~/.claude/CLAUDE.md) which are user-scoped and stay out of the repo.

Related stacks