Tinker AI
Read reviews
intermediate 7 min read

Cursor on Vue 3 Composition API: prompts and rules that match the framework

Published 2026-03-15 by Owner

The first month I used Cursor on a Vue 3 project, I rewrote about 30% of its output. Composition API patterns it produced were fine but inconsistent — sometimes ref, sometimes reactive, sometimes Options API leaking back in. The problem wasn’t Cursor; it was that Cursor’s training distribution skews React, and Vue 3 best practices are noticeably different from what gets generated by default.

Two rule files and a few prompt habits later, the rewrite rate dropped to maybe 5%. This is the setup that worked.

The framing problem

Vue 3 has multiple ways to write a component:

  • Options API (Vue 2 style, still supported)
  • Composition API with <script setup> (idiomatic for Vue 3)
  • Composition API with explicit defineComponent and setup() (verbose, less common)

Cursor’s default output drifts between these. Sometimes you get <script setup>, sometimes you get <script> with defineComponent, sometimes a hybrid that doesn’t quite work. None of these are wrong per se; the inconsistency is what produces churn.

The fix is to anchor Cursor on the variant you want, in writing, before you ever prompt.

A working .cursor/rules/general.mdc

---
description: Vue 3 + TypeScript conventions for this project
globs:
alwaysApply: true
---

# Stack

- Vue 3.4+ with `<script setup lang="ts">` syntax — never Options API, never explicit defineComponent
- Pinia for state management (no Vuex)
- Vue Router 4
- TypeScript strict mode
- pnpm

# Composition API patterns

- Use `ref` for primitives and arrays. Use `reactive` only for objects with stable keys.
- Always type `ref` explicitly when the type isn't inferred: `const count = ref<number>(0)`.
- Computed properties must be readonly: `const total = computed(() => ...)`.
- Watchers use `watch` with explicit source, not `watchEffect`, unless reactivity tracking matters.
- Lifecycle: `onMounted`, `onUnmounted`. No mounted() / created() Options API leakage.

# Don't write

- `defineComponent({ ... })` — use `<script setup>` instead
- `data()` functions — use refs at the top level
- `methods: { ... }` — use plain functions in `<script setup>`
- `computed: { ... }` Options-API style — use `computed(() => ...)`
- Mixins — use composables in `composables/` directory instead

# Component structure

```vue
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'

interface Props {
  initialCount?: number
}

const props = withDefaults(defineProps<Props>(), { initialCount: 0 })
const emit = defineEmits<{
  change: [value: number]
}>()

const count = ref(props.initialCount)
const doubled = computed(() => count.value * 2)

function increment() {
  count.value++
  emit('change', count.value)
}
</script>

<template>
  <button @click="increment">{{ doubled }}</button>
</template>

This is the idiomatic shape for every component in this project.


That's about 50 lines and covers the patterns I most often had to fix.

## Composables-specific rules

Vue 3's composables (the equivalent of React custom hooks) deserve their own rule:

```markdown
---
description: Composable conventions
globs: src/composables/**/*.ts
alwaysApply: false
---

# Composables

- Filename: `useFooBar.ts`, exporting a function `useFooBar()`
- Return an object with named refs/methods, not a tuple
- Document the SSR-safety: each composable must say in a comment whether it's SSR-safe or client-only
- For composables that wire to lifecycle, register cleanup in `onUnmounted`

Example shape:

```typescript
// SSR-safe (no DOM access).
export function useCounter(initial = 0) {
  const count = ref(initial)
  const increment = () => count.value++
  const reset = () => (count.value = initial)
  return { count, increment, reset }
}

Composables should be small and single-purpose. If a composable returns more than 8 things, split it.


## Pinia store rules

```markdown
---
description: Pinia store conventions
globs: src/stores/**/*.ts
alwaysApply: false
---

# Pinia stores

- Use the setup syntax (`defineStore('name', () => { ... })`), never the options syntax
- Filename matches the store name: `userStore.ts` exports `useUserStore`
- Async actions: throw on error, don't return `{ success, error }` tuples
- Persist needs are explicit — use `pinia-plugin-persistedstate` only when called for, not by default

Prompts that produce the right shape

With those rules in place, prompts can be much shorter than they’d otherwise need to be. A typical prompt for a new component:

Add a UserAvatar component that takes a user prop (User type from @/types) and 
displays the user's initials in a colored circle when no image is provided. Falls 
back to a default gray circle if neither image nor name is available.

Place it in src/components/user/UserAvatar.vue.

That’s the whole prompt. The rules cover script setup syntax, type strictness, file conventions, composable patterns. Cursor produces a component that matches the project, no rewriting needed.

Without the rules, the equivalent prompt would need to include “use <script setup>, type the prop with TypeScript, follow Vue 3 Composition API conventions” — and Cursor would still drift on details.

Specific patterns that confuse Cursor without rules

Three things Cursor regularly gets wrong on Vue projects, and how rules fix them:

v-model on custom components. Vue 3’s modelValue / update:modelValue convention is different from Vue 2’s. Without a rule, Cursor sometimes generates Vue 2 syntax. Rule: “Custom v-model uses modelValue prop and update:modelValue emit, not value/input.”

Template refs for DOM access. Cursor sometimes writes this.$refs.foo (Options API) or forgets to call .value on the ref. Rule: “Template refs are typed as ref<HTMLElement | null>(null) and accessed via .value after mount.”

Props with default values. Cursor often writes defineProps<Props>() and then runtime-checks for undefined. Rule: “Use withDefaults for optional props with defaults, not runtime checks.”

Each of these gets a one-line rule. Together they cover the usual Vue-specific drift.

The result, in time

For my Vue project — about 80 components, 200 commits over three months — the difference between rules-on and rules-off Cursor:

  • Without rules: I rewrote roughly 30% of generated code, mostly for stylistic and pattern issues
  • With rules: I rewrite roughly 5%, almost all for things genuinely outside what rules can guide (semantic correctness)

That’s about 25% of the time I was spending on Cursor output reverting back to me. Across a typical day with maybe 15 generated components, the savings are 30-45 minutes.

The rules took maybe two hours to write the first time, plus another hour of refining over the first week. Amortized, they paid back inside a week.

Beyond the rules: the prompting habit

One habit that matters even with rules: when adding a component, mention the existing components you want it to match.

Add UserAvatar following the same conventions as @/components/user/UserBadge.

Cursor opens UserBadge for context, sees the actual patterns, and produces something stylistically consistent. This works alongside the rules — rules cover the abstract patterns, the reference component covers the concrete details.

For Vue projects with consistent style, this is sometimes more effective than the rules alone. The model imitates patterns it sees in code more reliably than patterns it reads in prose.

What still goes wrong

Some Vue patterns Cursor still struggles with even with rules:

  • Complex provide/inject typing — the type inference doesn’t always come through cleanly
  • Composables with deep reactivity that need explicit toRefs — Cursor sometimes returns reactive objects directly, breaking destructuring
  • Slot type definitions in <script setup> — the syntax is recent enough that Cursor sometimes uses outdated forms

For these, I still write the code by hand or use Cursor as a sketch and rewrite the type-sensitive parts. The rules don’t make Cursor perfect on Vue. They make it consistent enough that the remaining cleanup is small and predictable.

That predictability is the actual win. With rules, Cursor outputs something close to your style, deterministically. Without rules, the output is good or bad depending on the day’s vibes. Predictability is what makes the tool feel reliable.