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
| Convention | Without CLAUDE.md | With CLAUDE.md |
|---|---|---|
| Component API style | Options API (data(), methods:) generated | <script setup lang="ts"> enforced |
| Vue and Nuxt imports | Manual import { ref, computed } from 'vue' | Relies on Nuxt auto-imports, no manual import |
| State management | Vuex patterns (mutations, namespaced modules) | Pinia defineStore() with direct state mutation in actions |
| Composable naming | Any name (getUser, fetchData, userHelper) | useXxx prefix required |
| Component naming | Mixed (user-card.vue, userCard.vue) | PascalCase filename (UserCard.vue) |
| Pinia store naming | useUserStore, UserStore, userModule | useXxxStore pattern enforced |
$fetch usage | Used everywhere including SSR setup | useFetch/useAsyncData in components, $fetch in server routes only |
| TypeScript in templates | Often 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.$routerorthis.$route(Options API pattern) - Define route meta types in
router/types.tsand 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. Useunknownwith type guards when the type is genuinely unknown. - No non-null assertions (
!) without an inline comment explaining why it is safe. - Use
interfacefor object shapes. Usetypefor 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.tssuffix (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_NAMEin component or composable code. - Public vars (exposed to client): prefix with
NUXT_PUBLIC_in .env, access viaconfig.public.varName - Private vars (server-only): prefix with
NUXT_in .env, access viaconfig.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>anddefinePageMetastructure - Scaffolding Pinia stores with setup syntax
- Creating server API routes with proper TypeScript types and error handling
- Adding Nuxt modules with correct
nuxt.config.tsregistration
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
| Concern | CLAUDE.md | Skills (.claude/commands/) |
|---|---|---|
| “Never use Options API” | Root CLAUDE.md | Not needed |
| ”Always add lang=‘ts’ to script” | Root CLAUDE.md | Not needed |
| ”Scaffold a new page with auth middleware” | Too procedural for CLAUDE.md | Skill: /new-page |
| ”Generate a Pinia store with CRUD actions” | Convention summary only | Skill: /new-store |
| ”Create a server API route with validation” | Error handling rules only | Skill: /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.
Related Articles
- CLAUDE.md Best Practices: A Complete Guide
- Claude Code React and Next.js Development Guide
- AI Coding Rules for React and Next.js Projects
- Browse AI coding rules in our gallery
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.