← All posts
·11 min read

Claude Code with Vue 3: Composition API Workflow

Claude CodeVueWorkflowSetup
Claude Code with Vue 3: Composition API Workflow

Why Vue 3 projects need project-specific Claude Code configuration

Claude Code knows Vue 3. It understands <script setup>, ref, computed, watch, the Options API, Pinia, Vue Router, and the difference between a composable and a component. It can scaffold a single-file component from a prompt and get the syntax right.

The problem is that this knowledge is generic. Without configuration, Claude Code produces components that compile but do not fit your project. It might reach for the Options API when your team has standardised on <script setup>. It might colocate state that belongs in a Pinia store. It might name composables inconsistently, skip your path aliases, or generate test files that use a different assertion style from the rest of your suite. Each of these is a small friction point on its own. Together they mean every Claude Code output needs a review pass before it can be used.

A project-specific CLAUDE.md removes that friction. You encode your decisions once and Claude Code applies them every session. This guide builds that configuration layer, then covers the component authoring loop, composable extraction workflow, Pinia store patterns, and testing setup that make Vue 3 development with Claude Code genuinely fast. If you are new to Claude Code entirely, the Claude Code setup guide covers installation and authentication first.

The Vue 3 CLAUDE.md

A useful Vue 3 CLAUDE.md answers five questions: how is the project structured, how are components authored, how are composables named and organised, how is state managed, and how are tests run? Answering these once in the project root gives Claude Code the context it needs to produce consistent output from the first session onward.

# Vue 3 project rules

## Project structure
- Components in src/components/{ComponentName}/{ComponentName}.vue
- Composables in src/composables/use{Name}.ts
- Pinia stores in src/stores/{name}.store.ts
- Types in src/types/{domain}.ts (no inline type exports from .vue files)
- Route views in src/views/{RouteName}.vue
- Path alias: @ maps to src/

## Component conventions
- script setup syntax exclusively. No Options API, no defineComponent wrapper.
- TypeScript in every component: <script setup lang="ts">
- Props defined with defineProps<{ ... }>() generic syntax. No runtime validators.
- Emits defined with defineEmits<{ ... }>() generic syntax.
- One component per file. Extract when a second component appears.
- Prefer ref() over reactive() for primitive values. Use reactive() for objects
  that are always referenced together.

## Composables
- File name: useFeatureName.ts. Function name matches file name.
- Composables return an object of refs and methods, never reactive().
- Composables do not render. They encapsulate logic that a component uses.
- Side effects in watchEffect or watch with explicit sources listed.
- Async composables use a loading ref and an error ref, not thrown exceptions.

## State management
- Local component state: ref / computed in script setup
- Shared state: Pinia stores in src/stores/
- Server data: composables wrapping fetch or a library like TanStack Query
- No Vuex. No global reactive() objects outside of stores.

## Testing
- Framework: Vitest + Vue Test Utils
- Run full suite: npx vitest run
- Watch mode: npx vitest
- Coverage: npx vitest run --coverage
- Test files colocated: {ComponentName}.test.ts next to the .vue file
- Composable tests: src/composables/{name}.test.ts
- Test user-visible behaviour, not implementation details.
- Mount with mount() for components that need a full DOM tree.
- Use shallowMount() only for components with many heavy children being tested in isolation.

## Hard rules
- No any types. TypeScript strict mode is on.
- No inline styles. CSS scoped in the SFC or a CSS module.
- All images need alt text. Decorative images: alt=""
- Run npx vue-tsc --noEmit after structural changes to catch type errors early.

Load this at the project root before your first Claude Code session. The output shifts immediately: components arrive with the right structure, composables follow naming conventions, stores land in the right directory. You spend time reviewing logic, not reformatting files.

The component authoring loop

The most productive Claude Code pattern for Vue 3 is a single pass from requirements to working component with tests. The prompt structure that produces consistent output:

"Create a ProductCard component. It receives a product: Product prop (type in src/types/product.ts). It displays the product image, name, price formatted with formatCurrency from src/utils/format.ts, and an add-to-cart button that emits an add-to-cart event with the product id. Follow the project component conventions."

With a complete CLAUDE.md in place, Claude Code will:

  • Create src/components/ProductCard/ProductCard.vue with <script setup lang="ts">
  • Define defineProps<{ product: Product }>() using the generic syntax
  • Define defineEmits<{ 'add-to-cart': [id: string] }>() correctly
  • Import Product from src/types/product.ts rather than redeclaring it inline
  • Import formatCurrency from src/utils/format.ts
  • Use scoped CSS for styles, not inline objects
  • Create a colocated ProductCard.test.ts that tests visible behaviour

Without the CLAUDE.md, you get a self-contained component with inline types, a defineComponent wrapper, and style bindings. Both work. Only one fits your project.

Iterating on a component

Revisions work best when the prompt describes the desired behaviour rather than the implementation change. "The add-to-cart button should be disabled when product.stock === 0 and its label should read 'Out of stock'" is specific and produces a precise result. "Make it handle out-of-stock" is ambiguous and produces inconsistent results.

For template changes involving v-if, v-show, or conditional class bindings, describe the state that triggers each branch. Claude Code maps state to template logic accurately. It maps vague adjectives like "hide it when appropriate" to whatever it guesses you mean, which is not the same thing.

Composable extraction

Vue components accumulate logic as features are added. A component that starts at 80 lines becomes 300 lines as data fetching, filtering, and side effects pile up. The correct response is composable extraction, and Claude Code handles it well when you frame the task correctly.

The pattern:

"This UserDashboard.vue component has data-fetching logic, permission checking, and a local search filter all mixed into the script setup block. Extract the data-fetching and permission checking into a useUserDashboard composable. Keep only rendering-related refs and template interaction in the component. The composable should return the user data, permission flags, the loading state, and the error state."

Claude Code reads the component, identifies logical groupings, moves them into a composable with a clean return object, and updates the component to consume the composable. The result is independently testable.

What Claude Code does well here is preserving implicit dependencies. If the data-fetching composable fires only after a permission check resolves, Claude maintains that ordering rather than extracting them as two independent pieces. This is the kind of structural reasoning that makes the tool useful beyond simple code generation.

Async composables

A consistent pattern for async composables keeps test setup manageable. In your CLAUDE.md, add a brief example:

## Async composable pattern

export function useProducts() {
  const data = ref<Product[]>([])
  const loading = ref(false)
  const error = ref<string | null>(null)

  async function fetchProducts() {
    loading.value = true
    error.value = null
    try {
      data.value = await productService.getAll()
    } catch (e) {
      error.value = e instanceof Error ? e.message : 'Unknown error'
    } finally {
      loading.value = false
    }
  }

  return { data, loading, error, fetchProducts }
}

Claude Code replicates this structure for any async composable once the example is in the CLAUDE.md. The loading and error refs make test assertions straightforward: set loading to true, assert the spinner renders; set error to a string, assert the error message appears.

Pinia store patterns

The most common state management mistake with Claude Code on Vue projects is not specifying where state should live. Without guidance, Claude defaults to ref inside the component for state that would be better in a Pinia store. On small projects this is harmless. On projects where multiple components need the same data, it produces duplicated fetch calls and out-of-sync UI state.

Your CLAUDE.md handles this by routing state to the correct layer. For Pinia stores, the prompt pattern that produces reliable output:

"Create a useCartStore Pinia store in src/stores/cart.store.ts using the setup store syntax. It should hold an array of CartItem (type in src/types/cart.ts). Expose: items (the array as readonly), itemCount (computed), totalPrice (computed using formatCurrency from src/utils/format.ts), addItem(item: CartItem) which increments quantity if the item already exists, removeItem(id: string), and clearCart()."

Claude Code generates a typed Pinia setup store with defineStore, the correct storeToRefs usage in components, and actions that match the described behaviour. It will not add persistence plugins or devtools configuration unless you ask, which is correct for a store that does not need them yet.

Setup store vs options store

The setup store syntax (using defineStore with a function) is consistent with the Composition API style your CLAUDE.md enforces. The options store syntax (state, getters, actions) is cleaner for simple cases but inconsistent with <script setup> conventions.

If your project uses setup stores, add one line to CLAUDE.md:

- Pinia stores use setup store syntax: defineStore('name', () => { ... })
- No options store syntax (state/getters/actions object form)

One rule, consistent output across all stores for the life of the project.

Testing Vue 3 components and composables

The testing workflow that produces the most value runs tests as part of the authoring loop, not after it. The component is not finished until its tests pass.

## After every component or composable change

1. Run npx vitest run src/components/{ComponentName}
2. If tests fail, fix before considering the task done.
3. If coverage on changed files drops below 80%, add tests.
4. Never stub internal composables without explaining why.

With this in CLAUDE.md, Claude Code runs tests automatically after each change. Failures surface during the task.

Component tests

Vue Test Utils tests written against the rendered DOM rather than component internals survive refactors. When you change the internal structure of a component but preserve its visible behaviour, tests written against wrapper.text(), wrapper.find(), and wrapper.trigger() continue to pass. Tests against internal refs do not.

// Good: tests visible behaviour
test('add to cart button is disabled when stock is 0', () => {
  const wrapper = mount(ProductCard, {
    props: { product: outOfStockProduct }
  })
  expect(wrapper.find('button').attributes('disabled')).toBeDefined()
  expect(wrapper.find('button').text()).toBe('Out of stock')
})

// Good: tests user interaction
test('emits add-to-cart with product id on button click', async () => {
  const wrapper = mount(ProductCard, {
    props: { product: inStockProduct }
  })
  await wrapper.find('button').trigger('click')
  expect(wrapper.emitted('add-to-cart')?.[0]).toEqual([inStockProduct.id])
})

Ask Claude Code to write tests in this style explicitly when starting a new component. The CLAUDE.md instruction "Test user-visible behaviour, not implementation details" points it in the right direction, but an explicit example in the first test session locks the pattern.

Composable tests

Composables are easier to test than components because they do not render. You call them, interact with the returned refs, and assert on state.

test('addItem increments quantity for existing items', () => {
  const { addItem, items } = useCartStore()
  addItem({ id: '1', name: 'Widget', price: 10, quantity: 1 })
  addItem({ id: '1', name: 'Widget', price: 10, quantity: 1 })
  expect(items.value.find(i => i.id === '1')?.quantity).toBe(2)
  expect(items.value.length).toBe(1)
})

This kind of test is fast, reliable, and has no DOM overhead. Ask Claude Code to write composable tests separately from component tests. They run faster in isolation and are easier to maintain.

Debugging Vue 3 issues with Claude Code

Debugging works best when you give Claude Code the error output rather than a description of the error. Paste the full browser console output, the Vue DevTools component tree, or the failing test output. Claude reads error messages the way a developer does: starting from the root cause in the stack trace, not the symptom in the last line.

For reactivity issues where the template is not updating when a value changes, the pattern that works:

"This template should update when items.value changes but it does not. Here is the composable: [paste]. Here is the template: [paste]. Find the reactivity break and fix it."

Common causes Claude Code identifies correctly: returning a non-reactive value from a composable instead of a ref, using reactive() and then spreading it (which loses reactivity), or mutating a prop directly. Each has a specific fix. Claude gives you the exact change, not a general explanation of Vue reactivity.

For TypeScript errors in <script setup>, paste the full vue-tsc output. Claude Code reads these accurately and resolves mismatched prop types, incorrect emit signatures, and missing generic type arguments without guessing.

What Vue developers get wrong first

Three mistakes appear consistently when Vue developers start using Claude Code without framework-specific configuration.

Not specifying the API. Without an explicit rule, Claude Code mixes Options API and Composition API across components. A codebase that started with <script setup> gradually accumulates defineComponent wrappers. One line in CLAUDE.md prevents this entirely.

Asking for full features in single prompts. "Build a product listing page with search, filtering, pagination, and cart functionality" produces something that compiles and has the right shape but ignores your type system, naming conventions, and composable patterns. The better approach is layered: types first, then composables, then components, then tests. Each layer is reviewable before the next one builds on top.

Using reactive() for everything. Developers coming from Vue 2 or React often reach for reactive() because it feels familiar. Claude Code sometimes follows this pattern when it appears in the existing codebase. The CLAUDE.md rule "Prefer ref() over reactive() for primitive values" eliminates this ambiguity and produces more predictable composable return shapes.

Getting the most from your Vue 3 workflow

The configuration in this guide produces a setup where Claude Code generates components that match your SFC conventions, composables that extract logic cleanly, Pinia stores that follow setup store patterns, and tests that remain valid after refactors.

The foundation is the CLAUDE.md template above. Add it to your project root before your first Claude Code session, run one component-to-composable-to-store cycle end-to-end, and adjust based on what Claude generates. Most Vue projects need two or three additional rules after the first session. After that, the output is consistent enough to review in seconds.

For the broader principles that apply across all Claude Code workflows, the Claude Code best practices guide covers session management, context control, and the mental models that make the difference between slow and fast development loops. If you want to extend the CLAUDE.md approach across multiple Vue projects without duplicating configuration, the Claude Code memory systems guide covers that pattern.

For teams evaluating whether Claude Code is the right tool for Vue development versus other AI coding assistants, the Claude Code vs Cursor comparison covers the trade-offs in depth. And if you are interested in what a production Claudify subscription adds on top of the base Claude Code experience, including pre-built skills for Vue 3 workflows, see the Claudify plans.

More like this

Ready to upgrade your Claude Code setup?

Get Claudify
Featured on Dofollow.Tools AI Toolz Dir