Claude Code Vue.js Nuxt CLAUDE.md AI Coding Pinia 2026

CLAUDE.md for Vue.js and Nuxt 3 Projects: Templates and Best Practices 2026

The Prompt Shelf ·

Vue 3 and Nuxt 3 have well-defined conventions that differ significantly from both Vue 2 and from React-world defaults. Claude Code, left without guidance, defaults to patterns it has seen most frequently in training data, and that causes concrete problems in Vue/Nuxt codebases.

Here is what goes wrong in practice. The model generates data() and methods: blocks instead of <script setup lang="ts">. It wraps components in export default defineComponent({}) because that pattern appears extensively in older documentation. It creates Vuex-style stores with state, mutations, and actions objects in a project that uses Pinia’s defineStore. It adds manual import { ref, computed } from 'vue' statements at the top of every file, not knowing that Nuxt auto-imports these globally. It mixes pages/ and app/ directory conventions from Nuxt 2 and Nuxt 3. It calls $fetch inside component setup functions that run during SSR, triggering double data fetching when useFetch or useAsyncData is the right tool.

None of these are model failures in a general sense. They are failures of context. A CLAUDE.md file gives Claude Code the context it needs to generate code that fits your project from the first line.

Section 1: Vue 3 + Nuxt 3 Conventions Claude Gets Wrong

Three areas generate the most friction between Claude Code defaults and real Vue/Nuxt projects.

1. Composition API vs Options API

Vue 3 shipped the Composition API as the preferred style, and Vue 3.2 introduced <script setup> as the clean syntactic sugar on top of it. The recommended pattern for any new component is:

<script setup lang="ts">
const props = defineProps<{ title: string }>()
const emit = defineEmits<{ close: [] }>()
</script>

Without CLAUDE.md context, Claude Code frequently generates Options API code because a large volume of Vue documentation, tutorials, and GitHub examples still use it. The model has no way to know your project policy. One explicit rule in CLAUDE.md eliminates the problem.

2. Nuxt Auto-Imports

Nuxt 3 auto-imports composables from composables/, components from components/, utilities from utils/, and all Vue and VueUse core functions globally. Manually importing these is not just unnecessary but actively counterproductive: it creates inconsistency, confuses tools that analyze import graphs, and sometimes causes subtle SSR/CSR mismatches when the wrong import path is used.

Claude Code adds manual imports by default because it cannot infer the auto-import system from code alone. A CLAUDE.md rule that states which imports to omit, and where to check the generated auto-import types, solves this immediately.

3. State Management Patterns

Projects using Pinia defineStore are built around a fundamentally different mental model than Vuex. Pinia stores have no mutations: state is mutated directly in actions, or via $patch. Stores are organized as flat objects, not nested modules. There are no namespaced module patterns.

Claude Code defaults to the mutation/action split because Vuex documentation is widespread and Pinia documentation is newer. Without an explicit directive, it writes stores that work but introduce patterns your team has moved away from.

Convention Comparison Table

ConventionWithout CLAUDE.mdWith CLAUDE.md
Component API styleOptions API (data(), methods:) generated<script setup lang="ts"> enforced
Vue and Nuxt importsManual import { ref, computed } from 'vue'Relies on Nuxt auto-imports, no manual import
State managementVuex patterns (mutations, namespaced modules)Pinia defineStore() with direct state mutation in actions
Composable namingAny name (getUser, fetchData, userHelper)useXxx prefix required
Component namingMixed (user-card.vue, userCard.vue)PascalCase filename (UserCard.vue)
Pinia store naminguseUserStore, UserStore, userModuleuseXxxStore pattern enforced
$fetch usageUsed everywhere including SSR setupuseFetch/useAsyncData in components, $fetch in server routes only
TypeScript in templatesOften absent or lang="js"lang="ts" on all <script setup> blocks

Section 2: Complete CLAUDE.md Template for Vue 3 SPA

This template covers a Vite-based Vue 3 SPA without Nuxt: TypeScript strict mode, Pinia for state, Vue Router 4, Vitest for testing, and scoped styles. Copy it to your project root and adjust the directory paths and dependency list to match your setup.

# Project: [Your Vue 3 App Name]

## Stack
- Vue 3.4+ with Composition API and script setup
- TypeScript 5+ (strict mode, no any)
- Vite 5+ (dev server and build tool)
- Vue Router 4 (history mode)
- Pinia for global state management
- Vitest + Vue Test Utils for unit and component tests
- Playwright for E2E tests (if applicable)
- CSS: scoped styles per component, no global utility framework

## Development Commands
- Dev server: `npm run dev` (Vite, port 5173)
- Build: `npm run build`
- Preview build: `npm run preview`
- Unit tests: `vitest run`
- Unit tests with coverage: `vitest run --coverage`
- Type check: `vue-tsc --noEmit`
- Lint: `eslint . --ext .vue,.ts,.tsx`

## Directory Structure
src/
  assets/           # Static assets (images, fonts, global CSS)
  components/       # Reusable UI components
    ui/             # Primitives: Button, Input, Modal, Badge
    features/       # Feature-scoped composite components
  composables/      # Vue composables (useXxx naming convention)
  router/           # Vue Router configuration
    index.ts        # Router instance
    routes.ts       # Route definitions
  stores/           # Pinia stores (useXxxStore naming convention)
  types/            # Shared TypeScript type definitions
  utils/            # Pure utility functions (no Vue dependencies)
  views/            # Route-level page components

## Component Conventions — MANDATORY

### API Style
- ALWAYS use `<script setup lang="ts">`. Never use Options API.
- NEVER use: data(), methods:, computed: as options, mounted:, created:,
  watch: as option, or `export default defineComponent({})`.
- If you encounter Options API in existing files, flag it in a comment
  but do NOT rewrite the component unless explicitly instructed.

### Props and Emits
- Define props with TypeScript generics: `defineProps<{ title: string; count?: number }>()`
- Define emits with TypeScript generics: `defineEmits<{ submit: [value: string]; close: [] }>()`
- Use withDefaults() when default prop values are needed:
  `const props = withDefaults(defineProps<Props>(), { count: 0 })`
- Destructure props with toRefs() when reactivity is needed in composables

### File and Component Naming
- Component files: PascalCase (`UserCard.vue`, `ModalOverlay.vue`)
- Composable files: camelCase with use prefix (`useUserSession.ts`)
- Store files: camelCase with store suffix (`userStore.ts`)
- All other files: kebab-case (`date-utils.ts`, `api-client.ts`)
- One component per file. No multi-component files.

### Template Style
- Use v-bind shorthand (`:prop`) and v-on shorthand (`@event`)
- Self-close tags with no children: `<MyComponent />`
- Use `<template>` wrappers with v-if/v-for; never put v-if and v-for
  on the same element
- Key all v-for loops; prefer stable IDs over array indices

## Pinia Store Conventions

- Name stores with useXxxStore pattern: `useUserStore`, `useCartStore`
- Use setup store syntax (function-based) rather than options syntax:

```ts
// CORRECT
export const useUserStore = defineStore('user', () => {
  const currentUser = ref<User | null>(null)
  const isAuthenticated = computed(() => currentUser.value !== null)

  async function login(credentials: LoginCredentials) {
    currentUser.value = await authApi.login(credentials)
  }

  function logout() {
    currentUser.value = null
  }

  return { currentUser, isAuthenticated, login, logout }
})

// INCORRECT — do not use options syntax
export const useUserStore = defineStore('user', {
  state: () => ({ currentUser: null }),
  mutations: { setUser(state, user) { state.currentUser = user } },
})
  • Mutate state directly inside actions. No mutations layer.
  • Expose only what consumers need via the return object.
  • Use $patch() for multi-field updates: store.$patch({ a: 1, b: 2 })

Vue Router 4 Conventions

  • Import router composables: const router = useRouter() / const route = useRoute()
  • Never access this.$router or this.$route (Options API pattern)
  • Define route meta types in router/types.ts and extend RouteMeta interface
  • Lazy-load route components: component: () => import('@/views/UserProfile.vue')
  • Use named routes for navigation: router.push({ name: 'user-profile', params: { id } })

TypeScript Rules

  • strict: true is required. Do not disable strictNullChecks or noImplicitAny.
  • No any. Use unknown with type guards when the type is genuinely unknown.
  • No non-null assertions (!) without an inline comment explaining why it is safe.
  • Use interface for object shapes. Use type for unions, intersections, and aliases.
  • Generic components use the defineProps<T>() pattern, not PropType.

CSS and Styles

  • All component styles use <style scoped>. Never use unscoped styles in components.
  • Global styles go in src/assets/styles/ and are imported in main.ts.
  • Use CSS custom properties for theming. Avoid hard-coded color values.
  • No inline styles in templates except for dynamic values that cannot be done with classes.

## Section 3: Complete CLAUDE.md Template for Nuxt 3 SSR

This template covers a Nuxt 3 project with TypeScript strict mode, Pinia for client state, `useState` for server-synchronized state, and Nuxt's auto-import system. The most important section is the auto-import prohibition, which prevents a large class of common errors.

```markdown
# Project: [Your Nuxt 3 App Name]

## Stack
- Nuxt 3.12+ (SSR mode, unless otherwise noted)
- Vue 3.4+ with script setup and Composition API
- TypeScript 5+ (strict mode)
- Pinia for persistent client state (via @pinia/nuxt)
- VueUse for utility composables (auto-imported)
- Tailwind CSS (via @nuxtjs/tailwindcss) — optional, remove if not used

## Development Commands
- Dev server: `nuxi dev` (port 3000)
- Build: `nuxi build`
- Static generation: `nuxi generate`
- Preview production build: `nuxi preview`
- Type check: `nuxi typecheck`
- Lint: `npx eslint . --ext .vue,.ts`

## CRITICAL: Auto-Import Rules

Nuxt 3 auto-imports the following. DO NOT manually import them:

From Vue (never import manually):
  ref, reactive, computed, watch, watchEffect, onMounted, onUnmounted,
  onBeforeMount, onBeforeUnmount, nextTick, toRef, toRefs, readonly,
  shallowRef, shallowReactive, defineComponent, defineAsyncComponent

From Nuxt (never import manually):
  useRouter, useRoute, useHead, useSeoMeta, definePageMeta, defineNuxtConfig,
  useFetch, useAsyncData, useLazyFetch, useLazyAsyncData, useNuxtApp,
  useRuntimeConfig, useState, navigateTo, abortNavigation,
  defineNuxtRouteMiddleware, defineEventHandler, readBody, getQuery,
  createError, useError

From VueUse (never import manually):
  useLocalStorage, useSessionStorage, useDark, useToggle, useWindowSize,
  useIntersectionObserver, useDebounceFn, useThrottleFn — and all others

From auto-imported composables (composables/ directory):
  All files in composables/ are auto-imported by filename. A file named
  useUserSession.ts exports useUserSession automatically.

To see the full auto-import list: check .nuxt/types/auto-imports.d.ts
(generated by running `nuxi dev` or `nuxi prepare`).

## Directory Structure
pages/              # File-based routing (replaces router config)
  index.vue         # Route: /
  about.vue         # Route: /about
  users/
    index.vue       # Route: /users
    [id].vue        # Route: /users/:id (dynamic segment)
components/         # Auto-imported components (PascalCase)
  ui/               # Primitives (Button.vue, Modal.vue)
  features/         # Feature-scoped composites
composables/        # Auto-imported composables (useXxx.ts)
layouts/            # Layout components (default.vue, auth.vue)
middleware/         # Route middleware
server/
  api/              # Server API routes (defineEventHandler)
  middleware/       # Server-only middleware
  utils/            # Server-only utilities
stores/             # Pinia stores (registered via @pinia/nuxt)
public/             # Static files served as-is
assets/             # Processed assets (Vite processes these)

## Pages and Routing
- Pages directory provides file-based routing. Do not create a router/index.ts.
- Use definePageMeta() at the top of each page for meta, layout, middleware:

```vue
<script setup lang="ts">
definePageMeta({
  layout: 'dashboard',
  middleware: ['auth'],
  title: 'User Profile',
})
</script>
  • Dynamic segments: [id].vue (required), [[id]].vue (optional)
  • Catch-all: [...slug].vue
  • Route groups use parentheses: (marketing)/ (directory name excluded from URL)

Data Fetching — Correct Patterns

Use the right tool for the context:

  • useFetch('/api/users') — component setup, SSR-friendly, deduplicated, returns { data, pending, error, refresh }. Use this as the default.
  • useAsyncData('key', () => $fetch('/api/users')) — when you need a custom key, transform option, or conditional fetching logic.
  • useLazyFetch / useLazyAsyncData — when you want the page to render immediately without blocking on the data (shows pending state).
  • $fetch('/api/users') — use ONLY inside server routes (server/api/), event handlers, or Pinia actions. Never in component setup on an SSR page (causes double fetching: once on server, once on client).

Server API Routes (server/api/)

  • Every file exports a single default handler via defineEventHandler:
// server/api/users/[id].get.ts
export default defineEventHandler(async (event) => {
  const id = getRouterParam(event, 'id')
  const body = await readBody(event)   // for POST/PATCH
  const query = getQuery(event)        // for GET query params
  return { user: await db.user.findUnique({ where: { id } }) }
})
  • File suffix determines HTTP method: .get.ts, .post.ts, .patch.ts, .delete.ts
  • Throw errors with: throw createError({ statusCode: 404, message: 'Not found' })
  • Server routes run in Node.js. Do not use browser APIs here.
  • Do not import Vue composables in server routes. They are server-only contexts.

State Management

Client state (Pinia) — for data that persists across navigation:

  • Use defineStore with setup syntax (function-based, see Vue 3 template for example)
  • Store ID must be unique: defineStore('user', ...)
  • Pinia is auto-imported via @pinia/nuxt — no manual createPinia() needed

Server-synchronized state (useState) — for data shared between SSR and CSR:

  • Use when initial value comes from the server and must hydrate to the client
  • Key must be unique across the app: const counter = useState('counter', () => 0)
  • Do not use useState as a replacement for Pinia; it has no actions or getters

Middleware (middleware/ directory)

  • Route middleware (runs before navigation):
// middleware/auth.ts
export default defineNuxtRouteMiddleware((to, from) => {
  const { isAuthenticated } = useUserStore()
  if (!isAuthenticated) {
    return navigateTo('/login')
  }
})
  • Global middleware: name the file with .global.ts suffix (runs on every route)
  • Named middleware: referenced in definePageMeta({ middleware: [‘auth’] })
  • Server middleware lives in server/middleware/ and runs on every server request

Environment Variables

  • ALWAYS use useRuntimeConfig() to access env vars in components and composables.
  • NEVER use process.env.VAR_NAME in component or composable code.
  • Public vars (exposed to client): prefix with NUXT_PUBLIC_ in .env, access via config.public.varName
  • Private vars (server-only): prefix with NUXT_ in .env, access via config.varName (only safe inside server/api/)

TypeScript Rules

  • strict: true required. No any. No non-null assertions without comments.
  • defineProps and defineEmits must include TypeScript generic types.
  • Server route handlers must type the return value explicitly.
  • Use NuxtError type for typed error handling: throw createError<NuxtError>(...)

## Section 4: Path-Scoped Rules for Vue and Nuxt Projects

As a project grows, a single CLAUDE.md becomes unwieldy and loads unnecessary context for focused tasks. Claude Code supports path-scoped rules files: markdown files in `.claude/rules/` with a `paths` frontmatter that controls when each file is loaded.

The recommended structure for a Nuxt 3 project:

.claude/ rules/ global.md # Git, PR, commit conventions — always loaded components.md # Component API rules — loads for components/** composables.md # Composable conventions — loads for composables/** pages.md # Routing and page conventions — loads for pages/** server.md # server/api rules — loads for server/** stores.md # Pinia conventions — loads for stores/**


Example frontmatter for each file:

```markdown
---
# .claude/rules/global.md
description: "Git workflow and PR conventions for this project"
# No paths: field — always loaded
---
---
# .claude/rules/components.md
description: "Vue 3 component conventions (script setup, props, emits, scoped styles)"
paths:
  - "components/**"
  - "src/components/**"
---
---
# .claude/rules/composables.md
description: "Composable naming, reactivity, and cleanup conventions"
paths:
  - "composables/**"
  - "src/composables/**"
---
---
# .claude/rules/pages.md
description: "Nuxt pages, definePageMeta, layouts, and navigation conventions"
paths:
  - "pages/**"
  - "layouts/**"
  - "middleware/**"
---
---
# .claude/rules/server.md
description: "Nuxt server API route patterns, defineEventHandler, error handling"
paths:
  - "server/**"
---
---
# .claude/rules/stores.md
description: "Pinia store patterns, defineStore setup syntax, action conventions"
paths:
  - "stores/**"
  - "src/stores/**"
---

When Claude Code opens a file in server/api/users.get.ts, it loads the root CLAUDE.md plus server.md. When it opens components/ui/Button.vue, it loads CLAUDE.md plus components.md. The global rules file loads in every session. This keeps context tight and relevant.

The practical benefit: server route conventions (never use Vue composables, always type return values, throw createError for errors) do not pollute the context when Claude Code is editing a component. Component conventions (script setup, scoped styles, PascalCase naming) do not appear when editing a Pinia store. Each context gets exactly what it needs.

Section 5: nuxt-skills and Claude Code Skills for Nuxt Teams

Beyond CLAUDE.md, two complementary tools shape how Claude Code behaves in Nuxt projects.

nuxt-skills Community Project

The nuxt-skills project (github.com/onmax/nuxt-skills) is a community-maintained collection of Claude Code skills built specifically for Nuxt workflows. It provides pre-built skill definitions for tasks like:

  • Generating Nuxt components with correct <script setup> and definePageMeta structure
  • Scaffolding Pinia stores with setup syntax
  • Creating server API routes with proper TypeScript types and error handling
  • Adding Nuxt modules with correct nuxt.config.ts registration

Skills in this project are designed to complement project-level CLAUDE.md files, not replace them. They encode procedural know-how (how to scaffold a component step by step), while CLAUDE.md encodes declarative constraints (what the result must look like).

CLAUDE.md vs Skills: What Goes Where

ConcernCLAUDE.mdSkills (.claude/commands/)
“Never use Options API”Root CLAUDE.mdNot needed
”Always add lang=‘ts’ to script”Root CLAUDE.mdNot needed
”Scaffold a new page with auth middleware”Too procedural for CLAUDE.mdSkill: /new-page
”Generate a Pinia store with CRUD actions”Convention summary onlySkill: /new-store
”Create a server API route with validation”Error handling rules onlySkill: /new-api-route

The split is: CLAUDE.md holds conventions that apply to every file Claude Code touches. Skills hold multi-step workflows that run on demand. A team using both gets precise, consistent output whether Claude Code is doing targeted edits or scaffolding new files from scratch.

To create a project skill, add a markdown file to .claude/commands/:

---
# .claude/commands/new-store.md
description: "Scaffold a new Pinia store with setup syntax and CRUD actions"
---

Create a new Pinia store in stores/ using the setup store pattern.

Requirements:
1. Filename: use camelCase + Store suffix (userStore.ts)
2. Export name: useXxxStore pattern (useUserStore)
3. Include: state refs, computed getters, async actions, error handling per CLAUDE.md
4. Add TypeScript types for all state and action parameters

Section 6: FAQ

Should I use CLAUDE.md or path-scoped rules for a large Vue app?

Use both together. The root CLAUDE.md holds project-wide conventions: stack declaration, forbidden API patterns (no Options API, no Vuex), TypeScript requirements, and naming rules that apply everywhere. Path-scoped rules files under .claude/rules/ hold directory-specific detail. A 500-line root CLAUDE.md is a burden on Claude Code’s context window and becomes harder to maintain. Splitting by directory keeps each context focused and accurate.

How do I stop Claude Code from generating Options API code?

Add an explicit, unambiguous prohibition to CLAUDE.md. A rule like “Never use the Options API (data(), methods:, computed: as options, mounted:, created:, watch: as option). All components must use <script setup lang=\"ts\">. If you see Options API in existing code, flag it but do not rewrite the file unless explicitly asked.” A positive example in the same section showing the correct <script setup> pattern reinforces the rule. Relying on Claude Code to infer style from existing files is less reliable than stating the rule directly.

Does CLAUDE.md work with Vue 2 and Vue 3 in the same project?

Vue 2 and Vue 3 can coexist during a migration using vue-demi or by structuring a monorepo where each package uses one Vue version. In these cases, a single root CLAUDE.md should acknowledge the split explicitly: state which directories use Vue 2 (Options API acceptable) and which use Vue 3 (Composition API required). Path-scoped rules files work well here. A legacy/ directory with a scoped rules file can retain Options API conventions, while src/ uses Composition API rules.

How do I handle Nuxt auto-imports in CLAUDE.md?

Provide an explicit list of what must not be imported manually. State the rule and give concrete examples: “Do not manually import ref, computed, watch, useRoute, useRouter, useFetch, useAsyncData, $fetch as a module, or any VueUse composable. Nuxt auto-imports all of these.” Also tell Claude Code where to look when it needs to verify what is available: .nuxt/types/auto-imports.d.ts is generated at dev-server start and contains the complete list. This prevents both over-importing (adding unnecessary imports) and under-importing (forgetting to add something that is genuinely not auto-imported).

Should I put Pinia store conventions in CLAUDE.md or a separate rules file?

A concise summary belongs in the root CLAUDE.md: store naming pattern, which syntax to use (setup stores vs options stores), and the rule against mutation layers. When Pinia conventions in your project are detailed enough to warrant more than 20 lines, move the full specification to .claude/rules/stores.md with a paths frontmatter targeting stores/**. The root CLAUDE.md summary ensures Claude Code picks up the basics in every session; the scoped rules file provides depth when Claude Code is actually working in that directory.

What’s the right CLAUDE.md structure for a Nuxt monorepo?

Place a root CLAUDE.md at the repository root with conventions shared across all packages: TypeScript version, commit message format, PR conventions, and shared tooling (linting, formatting). Then place a CLAUDE.md inside each Nuxt app subdirectory with app-specific context: the app’s page structure, which API endpoints it calls, its own composables and store structure, any environment variable naming conventions. Claude Code merges parent and child CLAUDE.md files when working inside a subdirectory, so the app-level file adds to the root conventions without replacing them.

How do I enforce TypeScript strictness in my Vue project with CLAUDE.md?

Specify required tsconfig settings and forbidden patterns together. Effective rules include: strict: true is non-negotiable and must not be overridden in any tsconfig.json or tsconfig.app.json; no any (use unknown with type guards when the type is genuinely unknown); no non-null assertions (!) without an inline comment; defineProps<TypeName>() and defineEmits<TypeName>() must include TypeScript generics, not PropType<T> workarounds; and API response types must be explicitly defined, not inferred from $fetch alone. These are the areas where AI-generated Vue TypeScript drifts most.

Can CLAUDE.md help prevent v-model breaking changes between Vue 2 and 3?

Yes. In Vue 3, v-model binds to modelValue prop and listens for update:modelValue emit. Vue 2 used value and input. Document the Vue 3 pattern explicitly in CLAUDE.md with an example:

<!-- Vue 3 v-model component -->
<script setup lang="ts">
const props = defineProps<{ modelValue: string }>()
const emit = defineEmits<{ 'update:modelValue': [value: string] }>()
</script>

Also note that Vue 3 supports multiple named v-model bindings (v-model:title, v-model:count) and that the .sync modifier from Vue 2 no longer exists. For projects actively migrating from Vue 2, these differences are worth enumerating in a CLAUDE.md migration notes section.


Catch Visual Regressions Before They Reach Production

When Claude Code generates or refactors Vue components, visual review is still a manual step. BugHerd lets your team pin bug reports directly on staging pages — no screenshot chains, no ambiguous Slack threads. Pairs cleanly with Claude Code’s code-level changes: Claude catches logic issues, BugHerd captures what broke visually.

Related Articles

Explore the collection

Browse all AI coding rules — CLAUDE.md, .cursorrules, AGENTS.md, and more.

Browse Rules