Claude Code for Vue 3 Development: Practical TypeScript Guide
Use Claude Code with Vue 3, TypeScript, Pinia, composables, forms, and Vitest in real project workflows.
Claude Code is useful in Vue development when you treat it as a project-aware coding partner, not only as a component generator. In a real Vue 3 codebase, the valuable work is reading an existing single-file component, preserving TypeScript types, moving shared logic into a composable, keeping state in Pinia, and adding Vitest coverage before the change reaches production.
Vue is flexible, which is also the source of many review problems. A codebase can contain Options API, Composition API, script setup, legacy Vuex ideas, Pinia stores, Nuxt conventions, and local helper functions at the same time. If the prompt is vague, Claude Code may produce code that looks clean but does not match the repository’s boundaries.
This guide shows a practical workflow for Claude Code + Vue 3 + TypeScript. The examples cover five common use cases: improving an existing Vue component, building a typed form, adding Pinia state management, extracting a composable, and writing Vitest tests. Keep the official Vue TypeScript with Composition API guide, Pinia documentation, and Vitest writing tests guide close while you work. For related Claude Code topics, see the TypeScript tips and testing strategy guide.
Project assumptions
The workflow below targets Vue 3, TypeScript, Vite, Pinia, and Vitest. It fits dashboards, internal tools, support desks, booking systems, and admin screens where forms, lists, filters, shared state, and tests matter more than visual novelty.
The important habit is to give Claude Code a harness, meaning a safe operating frame for the agent. A good harness names the files it can edit, the conventions it must keep, the commands it must run, and the mistakes it must avoid. Without that frame, Claude Code may solve the local task while adding any, changing copy, or moving logic into the wrong layer.
| Situation | Good Claude Code task | Human decision |
|---|---|---|
| Existing Vue refactor | Read the SFC, find repeated logic, add types | UI compatibility and release risk |
| Form implementation | Create local state, validation, submit flow | Product wording and API contract |
| Pinia state | Define store types, getters, actions, tests | Persistence and ownership boundary |
| Composable extraction | Move filtering and pagination logic | Whether reuse is real enough |
| Test addition | Cover state transitions and regressions | Risk level and coverage target |
Architecture at a glance
When Vue examples grow, the main question is not “Can Claude Code write the code?” The main question is “Where should each responsibility live?” A simple boundary keeps the generated diff reviewable.
flowchart LR
A[Vue SFC] --> B[Composable]
A --> C[Pinia Store]
B --> D[Vitest]
C --> D
E[Claude Code Prompt] --> A
E --> B
E --> C
E --> D
The single-file component owns the rendered form and user events. The composable owns reusable reactive logic such as filtering and pagination. The Pinia store owns shared application state. Vitest captures the behavior you do not want to regress. Claude Code works best when the prompt explicitly protects these boundaries.
Create a Vue 3 + TypeScript baseline
For a new experiment, start with the official Vue scaffolding path. For an existing repository, first make sure the current typecheck and unit tests pass so you can judge Claude Code’s changes against a clean baseline.
npm create vue@latest support-desk-vue -- --typescript --router --pinia --vitest
cd support-desk-vue
npm install
npm run dev
Do not prompt “make a modern Vue app” and hope the tool guesses your stack. Say whether the project uses npm, pnpm, or yarn; whether Vue Router is present; whether Pinia is the standard store; and which test command proves the change. In an existing app, ask Claude Code to inspect package.json, vite.config.ts, tsconfig.app.json, src/components, and src/stores before editing.
Use case 1: build a typed form SFC
Forms are a useful first target because they combine many Vue concepts: v-model, props, emits, validation, async submit state, and store actions. The example below can live at src/components/TicketForm.vue. It avoids prop mutation, keeps editable values in local refs, and emits the saved ticket id after the store action succeeds.
<script setup lang="ts">
import { computed, ref } from 'vue';
import { useTicketStore, type TicketPriority } from '@/stores/ticketStore';
const props = withDefaults(
defineProps<{
initialTitle?: string;
defaultPriority?: TicketPriority;
}>(),
{
initialTitle: '',
defaultPriority: 'medium',
},
);
const emit = defineEmits<{
submitted: [id: string];
}>();
const store = useTicketStore();
const title = ref(props.initialTitle);
const description = ref('');
const priority = ref<TicketPriority>(props.defaultPriority);
const touched = ref(false);
const titleError = computed(() => {
if (!touched.value) return '';
if (title.value.trim().length < 5) return 'Use at least 5 characters.';
return '';
});
const descriptionError = computed(() => {
if (!touched.value) return '';
if (description.value.trim().length < 20) return 'Use at least 20 characters.';
return '';
});
const canSubmit = computed(() => {
return (
title.value.trim().length >= 5 &&
description.value.trim().length >= 20 &&
!store.isSaving
);
});
async function submit() {
touched.value = true;
if (!canSubmit.value) return;
const ticket = await store.saveTicket({
title: title.value.trim(),
description: description.value.trim(),
priority: priority.value,
});
title.value = '';
description.value = '';
priority.value = props.defaultPriority;
touched.value = false;
emit('submitted', ticket.id);
}
</script>
<template>
<form class="ticket-form" @submit.prevent="submit">
<label>
Title
<input v-model="title" name="title" @blur="touched = true" />
</label>
<p v-if="titleError" class="error">{{ titleError }}</p>
<label>
Description
<textarea v-model="description" name="description" @blur="touched = true" />
</label>
<p v-if="descriptionError" class="error">{{ descriptionError }}</p>
<label>
Priority
<select v-model="priority" name="priority">
<option value="low">Low</option>
<option value="medium">Medium</option>
<option value="high">High</option>
</select>
</label>
<button type="submit" :disabled="!canSubmit">
{{ store.isSaving ? 'Saving...' : 'Create ticket' }}
</button>
</form>
</template>
When asking Claude Code to write this kind of component, include negative instructions: do not mutate props, do not introduce any, do not add a UI library, and do not move API details into the component. Negative constraints are not micromanagement. They prevent the most expensive cleanup work.
Use case 2: move shared state into Pinia
The form should not own the ticket list if multiple screens need that data. Pinia gives Vue 3 projects a typed, composable-friendly state layer. The following store keeps the shared list, a saving flag, a getter for open tickets, and actions for adding and closing tickets.
import { computed, ref } from 'vue';
import { defineStore } from 'pinia';
export type TicketPriority = 'low' | 'medium' | 'high';
export type TicketStatus = 'open' | 'closed';
export interface Ticket {
id: string;
title: string;
description: string;
priority: TicketPriority;
status: TicketStatus;
createdAt: string;
}
export type NewTicketInput = Pick<Ticket, 'title' | 'description' | 'priority'>;
export const useTicketStore = defineStore('tickets', () => {
const tickets = ref<Ticket[]>([]);
const isSaving = ref(false);
const openTickets = computed(() => {
return tickets.value.filter((ticket) => ticket.status === 'open');
});
function addTicket(input: NewTicketInput) {
const ticket: Ticket = {
id: crypto.randomUUID(),
...input,
status: 'open',
createdAt: new Date().toISOString(),
};
tickets.value.unshift(ticket);
return ticket;
}
async function saveTicket(input: NewTicketInput) {
isSaving.value = true;
try {
await new Promise((resolve) => setTimeout(resolve, 150));
return addTicket(input);
} finally {
isSaving.value = false;
}
}
function closeTicket(id: string) {
const ticket = tickets.value.find((item) => item.id === id);
if (ticket) ticket.status = 'closed';
}
return {
tickets,
isSaving,
openTickets,
addTicket,
saveTicket,
closeTicket,
};
});
In production, the async action would call a typed API client. Still, ask Claude Code to stabilize the store shape before wiring the real network call. That makes review easier because you can first approve the domain model, then the transport behavior, then the error handling.
Use case 3: extract a composable
A composable is a reusable function that contains Vue reactive logic. In plain language, it is the part of the screen that is not the screen. Filtering, priority selection, and pagination are good candidates because several list views may need them.
import { computed, ref, watch, type Ref } from 'vue';
import type { Ticket, TicketPriority } from '@/stores/ticketStore';
export function useFilteredTickets(tickets: Ref<Ticket[]>) {
const query = ref('');
const selectedPriority = ref<TicketPriority | 'all'>('all');
const currentPage = ref(1);
const pageSize = ref(10);
const filteredTickets = computed(() => {
const normalizedQuery = query.value.trim().toLowerCase();
return tickets.value.filter((ticket) => {
const matchesQuery =
normalizedQuery.length === 0 ||
ticket.title.toLowerCase().includes(normalizedQuery) ||
ticket.description.toLowerCase().includes(normalizedQuery);
const matchesPriority =
selectedPriority.value === 'all' ||
ticket.priority === selectedPriority.value;
return matchesQuery && matchesPriority;
});
});
const totalPages = computed(() => {
return Math.max(1, Math.ceil(filteredTickets.value.length / pageSize.value));
});
const pagedTickets = computed(() => {
const start = (currentPage.value - 1) * pageSize.value;
return filteredTickets.value.slice(start, start + pageSize.value);
});
watch([query, selectedPriority], () => {
currentPage.value = 1;
});
return {
query,
selectedPriority,
currentPage,
pageSize,
filteredTickets,
pagedTickets,
totalPages,
};
}
The watch call is intentionally narrow. It resets the page when filter inputs change. Derived values such as filteredTickets, totalPages, and pagedTickets stay in computed. This is a useful rule to include in Claude Code prompts: use computed for derivation and watch only for explicit side effects.
Use case 4: add Vitest coverage
As implementation becomes faster, regression tests become more important. Start with the store because it is stable, typed, and close to business behavior. The test below checks that a ticket can be created and then closed.
import { createPinia, setActivePinia } from 'pinia';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { useTicketStore } from './ticketStore';
describe('useTicketStore', () => {
beforeEach(() => {
setActivePinia(createPinia());
vi.stubGlobal('crypto', {
randomUUID: () => 'ticket-1',
});
});
it('adds a new open ticket', () => {
const store = useTicketStore();
const ticket = store.addTicket({
title: 'Cannot submit expense form',
description: 'The submit button stays disabled after all fields are filled.',
priority: 'high',
});
expect(ticket.id).toBe('ticket-1');
expect(store.tickets).toHaveLength(1);
expect(store.openTickets).toHaveLength(1);
});
it('closes an existing ticket', () => {
const store = useTicketStore();
const ticket = store.addTicket({
title: 'Profile image is broken',
description: 'The image URL returns 404 on the account settings page.',
priority: 'medium',
});
store.closeTicket(ticket.id);
expect(store.openTickets).toHaveLength(0);
expect(store.tickets[0]?.status).toBe('closed');
});
});
Claude Code often writes more tests than you need if you ask broadly. Ask for the behaviors that would hurt if they regressed: validation boundaries, disabled submit state, successful save, failed save, and store state transitions. That produces tests that protect real work instead of snapshots that simply freeze markup.
Use case 5: refactor an existing Vue component safely
Existing Vue code is where Claude Code saves the most time, but only if you avoid a giant rewrite. A safe sequence is: read the current component, list risks, add missing types, extract one composable, move shared state to Pinia if needed, then add tests. Each step should leave the app runnable.
You are working in a Vue 3 + TypeScript + Pinia project.
Goal:
Refactor the ticket form so validation logic is typed, reusable, and covered by Vitest.
Allowed files:
- src/components/TicketForm.vue
- src/composables/useFilteredTickets.ts
- src/stores/ticketStore.ts
- src/stores/ticketStore.test.ts
Rules:
- Use <script setup lang="ts">.
- Do not mutate props directly.
- Do not introduce any.
- Prefer computed for derived values.
- Use watch only for explicit side effects.
- Keep UI text unchanged unless a validation message is missing.
Verification:
- npm run typecheck
- npm run test:unit
After editing, explain the risk areas and the tests you added.
This prompt separates what Claude Code should decide from what the product owner or maintainer must decide. Claude Code can propose the extraction and write tests. Humans should confirm UI copy, API contracts, accessibility wording, and release timing.
Common pitfalls to watch
The first pitfall is mixing Options API and Composition API without a migration plan. Vue supports both, but a component that mixes data, methods, setup, and script setup becomes difficult to review. Keep the existing style for a small fix, or migrate one component at a time.
The second pitfall is misunderstanding ref and reactive. ref is usually easier for primitives, arrays, and values that may be replaced. reactive can work well for cohesive objects, but destructuring can break expectations. For teams new to Vue 3, a ref-first rule is easier to review.
The third pitfall is mutating props. A child component should not silently rewrite parent-owned state. Use local refs for draft values, emit events back to the parent, or use defineModel when the project has agreed on that pattern.
The fourth pitfall is overusing watch. If a value can be derived, use computed. Reserve watch for side effects such as synchronizing a route query, resetting a page, calling an API, or writing to storage.
The fifth pitfall is letting types collapse into any. Claude Code may add as any to move past a type error. Review every any, unknown, assertion, and empty array inference before accepting the diff.
The sixth pitfall is splitting SFCs too aggressively. A dozen tiny components can create more props and emits than clarity. Extract logic into composables first, and split UI only when the component is reused or meaningfully independent.
The seventh pitfall is missing tests. Generated Vue code can look polished while failing edge cases. For a form, cover short input, valid input, saving state, success, failure, and the resulting Pinia state.
Practical workflow and CTA
In a real repository, do not start with “build the whole screen.” Start by asking Claude Code to inspect the current component and report risks. Then make one typed change, run the checks, and continue. This keeps the diff reviewable and prevents a single prompt from mixing UI, state, API, and test changes.
For teams, put the rules into CLAUDE.md: Vue components use script setup, shared state uses Pinia setup stores, derived values use computed, side effects use watch, and the required checks are npm run typecheck and npm run test:unit. ClaudeCodeLab can help turn those rules into a repeatable review workflow through Claude Code training and consultation.
Summary
Claude Code can speed up Vue 3 development across existing refactors, forms, Pinia stores, composables, and tests. The difference between a useful diff and a cleanup burden is the prompt boundary. Tell it which files are allowed, which Vue style to use, what not to change, and how to verify the result.
Beginners should start with one SFC, one composable, one store, and one test file. That small loop teaches Vue’s mental model while still producing production-shaped code.
What happened when I tried it
I tested this workflow in a small Vue 3 + TypeScript project. Asking Claude Code for the form, store, composable, and tests in one request produced a larger diff that took longer to review. Splitting the work into SFC, Pinia, composable, then Vitest made the result easier to check. The most effective prompt constraints were “do not mutate props,” “do not introduce any,” and “use computed for derived values.” The resulting code was easier to validate with npm run typecheck and npm run test:unit.
Free PDF: Claude Code Cheatsheet
Enter your email and download the one-page Claude Code cheatsheet for commands, review habits, and safe workflows.
We handle your data with care and never send spam.
Level up your Claude Code workflow
Start with the free PDF, use Gumroad guides when you need repeatable workflows, and book consultation when rollout or revenue paths need human judgment.
About the Author
Masa
Engineer focused on practical Claude Code workflows. Runs claudecode-lab.com, a 10-language technical media site.
Related Posts
Claude Code Obsidian to CLAUDE.md Workflow: Stop Re-explaining Context
Turn Obsidian working notes into concise CLAUDE.md operating notes that make Claude Code sessions easier to resume.
Claude Code Revenue CTA Routing: Send Articles to PDF, Gumroad, and Consultation
A Claude Code workflow for routing article readers to the free PDF, Gumroad products, or consultation by intent.
Claude Code Team Handoff Rules: Review Evidence, Permissions, Rollback, and Revenue Paths
A practical Claude Code handoff format for team review, proof, permission rules, rollback, free PDF, Gumroad, and consultation paths.
Related Products
50 Battle-Tested Claude Code Prompt Templates
Copy, paste, ship. 50 production-ready prompts.
Use proven prompts for code review, refactoring, testing, documentation, debugging, architecture, and incident response.
The Complete Claude Code Setup & Configuration Guide
From install to team-ready workflow.
A practical guide to installation, CLAUDE.md, hooks, MCP servers, permissions, IDE setup, and CI/CD workflows.