Claude Code for Vue.js & Nuxt — Complete Setup & Workflow Guide
Claude Code works seamlessly with Vue.js and Nuxt projects when configured to understand your Composition API patterns, Pinia stores, and component conventions. This guide covers the CLAUDE.md template for Vue 3 and Nuxt 3 projects, automated Vitest hooks, Pinia workflow patterns, and Nuxt server-side rendering setup — so Claude generates idiomatic Vue code on the first try.
Vue 3 CLAUDE.md Template
# Project: [Your Vue App Name]
## Stack
- Vue 3 with Composition API and <script setup> SFCs
- TypeScript (strict mode enabled)
- Build: Vite 5.x
- Router: Vue Router 4 (history mode)
- State: Pinia with setup stores
- HTTP: @tanstack/vue-query + native fetch (or axios — specify)
- CSS: Tailwind CSS 3.x (or UnoCSS / Vuetify — specify)
- Testing: Vitest + @vue/test-utils
## Commands
```bash
npm run dev # dev server (vite)
npm run build # production build
npm run typecheck # vue-tsc --noEmit
npm run test # vitest run
npm run test:watch # vitest (watch mode)
npm run lint # eslint . --fix
```
## Conventions
- All components: <script setup lang="ts"> — never use Options API
- Props: defineProps<{ propName: Type }>() with TypeScript generics
- Emits: defineEmits<{ eventName: [payload: Type] }>()
- Composables: useXxx() functions in src/composables/, one concern each
- Pinia stores: setup store style (defineStore with setup function)
- API calls: always in composables or Pinia actions, never in template or setup directly
- Naming: PascalCase components, camelCase composables (useAuth.ts), kebab-case in templates
## Project structure
- src/
- components/ — shared, reusable UI components
- views/ — route-level page components
- composables/ — useXxx composables
- stores/ — Pinia store modules
- services/ — API layer (pure functions)
- types/ — TypeScript interfaces and types
- router/ — Vue Router config
- assets/ — static assets
- tests/ — Vitest unit tests (mirror src/ structure)
Automated Vitest + Type-Check Hooks
Add PostToolUse hooks so Claude sees test failures and type errors immediately:
// .claude/settings.json
{
"allowedTools": ["Edit", "Write", "Bash", "Read", "Glob", "Grep"],
"hooks": [
{
"event": "PostToolUse",
"matcher": "Edit|Write",
"hooks": [{
"type": "command",
"command": "cd $PROJECT_ROOT && npx vitest run --reporter=verbose 2>&1 | tail -30"
}]
}
]
}
TypeScript check hook
{
"event": "PostToolUse",
"matcher": "Write",
"hooks": [{
"type": "command",
"command": "cd $PROJECT_ROOT && npx vue-tsc --noEmit 2>&1 | head -30"
}]
}
Run
vitest run --changed in your hook to only test files that changed. For monorepos or large projects, this can reduce feedback time from 30s to 2s.Composition API & Component Patterns
Specify your Vue conventions to get consistent idiomatic output from Claude:
| Pattern | CLAUDE.md instruction |
|---|---|
| Props typing | "Define props with TypeScript generics: defineProps<{ label: string; count?: number }>()" |
| Emit typing | "Define emits with TypeScript: defineEmits<{ select: [id: string]; close: [] }>()" |
| Two-way binding | "Use defineModel() (Vue 3.4+) for v-model props instead of manual prop + emit" |
| Async data | "Use @tanstack/vue-query useQuery for server state; reserve Pinia for client-only UI state" |
| Provide / Inject | "Use typed injection keys (InjectionKey<T> in src/types/injection-keys.ts)" |
| Composable structure | "Each composable: one concern, returns only what consumers need, documented with JSDoc" |
New page + Pinia store
claude "add a UserDashboard page.
Route: /dashboard (add to router/index.ts, protected with auth guard).
View: src/views/UserDashboard.vue — layout with sidebar + main content grid.
Store: src/stores/dashboard.ts — Pinia setup store with:
- state: projects (Project[]), isLoading, error
- action: fetchProjects() — calls GET /api/projects, handles errors
- getter: activeProjects (filter by status === 'active')
Use @tanstack/vue-query for the fetch.
Write Vitest unit tests for the store actions."
Reusable composable with API call
claude "create a useInfiniteScroll composable in src/composables/useInfiniteScroll.ts.
Accepts: fetchFn (page: number) => Promise<T[]>, and options: { pageSize?: number }.
Returns: items (Ref<T[]>), isLoading, hasMore, loadMore().
Uses IntersectionObserver to trigger loadMore() when a sentinel element is visible.
Accepts a sentinelRef parameter (Ref<HTMLElement | null>).
Write Vitest tests using vi.fn() mocks for fetchFn."
Pinia Store Workflows
# CLAUDE.md additions for Pinia
## Pinia conventions
- Always use setup store syntax (not options stores)
- Store ID: camelCase matching the file name (useAuthStore → 'auth')
- Actions that call APIs: async, throw errors (caller handles with try/catch)
- Persist sensitive-free state with pinia-plugin-persistedstate
- Never mutate state outside of actions (even in tests — call the action)
Auth store example prompt
claude "add a useAuthStore in src/stores/auth.ts.
State: user (User | null), token (string | null), isLoading.
Actions:
- login(credentials: LoginPayload): POST /api/auth/login, persist token to localStorage
- logout(): clear state, remove token, redirect to /login
- refreshToken(): POST /api/auth/refresh, update token silently
- fetchCurrentUser(): GET /api/auth/me, set user state
Persist: token only (not user object) via pinia-plugin-persistedstate.
Write Vitest tests with a mocked fetch."
Nuxt 3 Specific Setup
# CLAUDE.md additions for Nuxt 3
## Nuxt conventions
- Framework: Nuxt 3 with SSR (not SPA mode)
- Auto-imports: ON — never write import { ref } from 'vue' (Nuxt auto-imports it)
- Data fetching: useFetch for SSR-compatible requests; $fetch for client-only
- Server routes: server/api/ directory (TypeScript with defineEventHandler)
- Layouts: layouts/default.vue + layouts/auth.vue; switch with definePageMeta
- Middleware: middleware/auth.ts for route guards (runs on server + client)
- Modules: @nuxtjs/tailwindcss, @pinia/nuxt, @nuxt/image — see nuxt.config.ts
## Commands
npx nuxi dev # dev server
npx nuxi build # production build
npx nuxi typecheck # type check
npx nuxi generate # static site generation
claude "add a /blog/[slug] dynamic page in Nuxt 3.
Fetch: useFetch('/api/posts/' + slug.value) — handle 404 with createError.
SEO: useSeoMeta with title, description from the post.
Layout: use the 'blog' layout (create layouts/blog.vue if missing).
Server route: server/api/posts/[slug].get.ts — query from posts[] array mock.
Write types in types/post.ts."
5 Tips for Vue.js + Claude Code
- Paste your
package.jsondependencies section into CLAUDE.md. Claude will use your exact versions of Vue Router, Pinia, VueUse, and other libraries rather than generating code for an incompatible API version. - Tell Claude which VueUse composables you already use: "VueUse installed — use useLocalStorage, useDebounceFn, useIntersectionObserver from @vueuse/core instead of reimplementing them." This prevents redundant custom implementations.
- For Nuxt projects, remind Claude that Nuxt auto-imports components and composables: "All components in components/ are auto-imported. All composables in composables/ are auto-imported. Do NOT add import statements for these." Otherwise Claude will add manual imports that conflict with Nuxt's auto-import system.
- Specify your CSS approach explicitly. Vue projects can use scoped styles, Tailwind, CSS modules, or a component library. "CSS: Tailwind CSS utility classes in template — no <style> blocks unless absolutely necessary" prevents Claude mixing approaches.
- When asking Claude to generate component tests, specify the testing pattern: "Test with @vue/test-utils mount(), not shallowMount(). Use screen queries from @testing-library/vue." Consistent testing patterns make it much easier to review generated tests.