Claude CodeでVue 3開発を効率化する実践ガイド
Claude CodeでVue 3とTypeScriptの改修、フォーム、Pinia、Composable、テストを進める実務手順。
Vue 3の開発でClaude Codeを使う価値は、コンポーネントを一瞬で生成することだけではありません。既存の画面を読み、状態管理の境界を見直し、TypeScriptの型を崩さずに改修し、最後にテストまで追加する一連の作業を短くできる点にあります。
ただし、Vueは書き方の幅が広いフレームワークです。Options API、Composition API、script setup、Pinia、Composable、Vue Router、Vite、Nuxtの流儀が混ざると、Claude Codeに曖昧な指示を出しただけでは「動くけれど保守しにくい」コードになりがちです。
この記事では、Claude Code + Vue 3 + TypeScriptを実務で使う前提で、既存Vue改修、フォーム実装、Pinia状態管理、Composable抽出、Vitestテスト追加までをまとめます。Vueの基礎を固めたい場合はVue公式のComposition APIとTypeScriptガイド、状態管理はPinia公式ドキュメント、テストはVitest公式ガイドを併読してください。Claude Code側の型設計はTypeScript活用テクニック、検証の広げ方はテスト戦略ガイドも参考になります。
この記事で扱うVue開発の前提
対象はVue 3、TypeScript、Vite、Pinia、Vitestを使う中小規模の業務アプリです。たとえば問い合わせ管理、社内申請、予約管理、管理画面のように、フォーム、一覧、検索、状態共有、権限、テストが絡む画面を想定します。
Claude Codeに渡す文脈は、単に「Vueでフォームを作って」では足りません。どのファイルを触ってよいか、既存の命名規則は何か、refとreactiveの使い分け、propsを直接更新しない方針、Pinia storeに置く状態、Composableに切り出すロジック、実行する確認コマンドまで伝えます。
ここでいうharnessは「エージェントを安全に動かす足場」です。Claude Codeにリポジトリ全体を自由に触らせるのではなく、対象ファイル、禁止事項、テストコマンド、レビュー観点を足場として与えると、差分が読みやすくなります。
| 場面 | Claude Codeに任せること | 人間が決めること |
|---|---|---|
| 既存Vue改修 | SFCの読解、重複ロジックの抽出、型の補強 | UI仕様、互換性、影響範囲 |
| フォーム実装 | 入力状態、エラー表示、送信中状態、テスト | バリデーション文言、API仕様 |
| Pinia状態管理 | storeの型、getter、action、テスト | 永続化の要否、責務分担 |
| Composable抽出 | フィルタ、ページング、再利用ロジック | 抽出しすぎない境界 |
| テスト追加 | 正常系、異常系、回帰テストの雛形 | どこまで保証するか |
全体像を図でつかむ
コード例が増えるほど、どの責務をどこに置くかが重要になります。Vueでは「画面SFC」「Composable」「Pinia store」「テスト」の境界を明確にすると、Claude Codeの修正もレビューしやすくなります。
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
この形にしておくと、フォームの見た目はSFC、検索やページングはComposable、複数画面で共有するデータはPinia、壊したくない振る舞いはVitestに置けます。Claude Codeには「この境界を守って」と明示できます。
ViteでVue 3 + TypeScript環境を作る
新規の検証環境なら、まずViteのVueテンプレートで最小構成を作ります。既存プロジェクトで作業する場合も、Claude Codeに依頼する前にnpm run typecheckやnpm run test:unitが通る状態を確認しておくと、生成された差分を評価しやすくなります。
npm create vue@latest support-desk-vue -- --typescript --router --pinia --vitest
cd support-desk-vue
npm install
npm run dev
この時点でClaude Codeに「最新のVue 3構成で作って」と丸投げしない方が安全です。プロジェクトで使うパッケージマネージャー、ルーティングの有無、Piniaを使うか、テストをVitestにするかを明示します。既存案件では、package.json、tsconfig.app.json、vite.config.ts、src/stores、src/componentsを読ませてから作業させます。
フォームSFCを実装する
最初のユースケースは、問い合わせチケットを登録するフォームです。フォームはVue初心者がつまずきやすい題材です。v-model、props、emit、送信中状態、バリデーション、Pinia actionが一箇所に集まるため、Claude Codeに境界をはっきり伝える必要があります。
以下はsrc/components/TicketForm.vueとして使える例です。propsを直接変更せず、入力値はローカルのrefで持ち、送信後にstoreへ渡しています。
<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>
Claude CodeにこのレベルのSFCを書かせるときは、「UIライブラリは使わない」「propsは直接変更しない」「送信処理はPinia actionへ寄せる」「anyを使わない」といった禁止条件を先に出します。これだけで、後から手で直す時間が大きく減ります。
Pinia storeで状態管理を分離する
2つ目のユースケースはPiniaです。複数画面で共有するチケット一覧、保存中フラグ、未完了チケットの算出はSFCではなくstoreに置きます。PiniaはVue 3のComposition APIと相性が良く、TypeScriptでも型を追いやすいのが利点です。
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,
};
});
Claude Codeにstoreを作らせるときは、API通信まで一気に書かせないのがコツです。まず同期処理で状態の形を固め、次にAPIクライアントを差し込み、最後にエラー処理とリトライを足すとレビューしやすくなります。
Composableに検索とページングを抽出する
3つ目のユースケースはComposable抽出です。Composableは、Vueのリアクティブなロジックを関数として再利用する方法です。初心者向けに言えば「画面からロジックだけを取り出した部品」です。
検索、優先度フィルタ、ページングはUIから独立させやすいため、useFilteredTickets.tsに切り出します。
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,
};
}
ここではwatchを1つだけ使っています。検索条件が変わったらページを1に戻す、という副作用が明確だからです。逆に、単なる値の加工をwatchで書くと追跡が難しくなります。Claude Codeには「派生値はcomputed、外部への副作用だけwatch」と伝えると安定します。
Vitestで回帰テストを追加する
4つ目のユースケースはテスト追加です。Claude Codeで実装速度が上がるほど、回帰テストの価値が上がります。フォームのUIテストを広く書く前に、まずPinia storeの状態遷移を小さく確認します。
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にテストを書かせる場合も、スナップショットを大量に増やすより、壊れると困る状態遷移を指定する方が実務で役に立ちます。
Claude Codeへの依頼文テンプレート
既存Vue改修では、依頼文の質が差分の質を決めます。以下のように、対象ファイル、禁止事項、期待する出力、確認コマンドをまとめると安定します。
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.
ポイントは「Claude Codeに考えさせる部分」と「人間が決める部分」を分けることです。バリデーションの文言、既存UIの互換性、API仕様は人間が決め、型の整備、Composable抽出、テストの追加はClaude Codeに任せます。
よくある落とし穴
1つ目の落とし穴は、Options APIとComposition APIの混在です。Vue 3ではどちらも使えますが、同じ画面でdata、methods、setup、script setupが混ざると、Claude Codeも人間も変更箇所を追いにくくなります。既存がOptions APIなら段階的に寄せる、Composition APIへ移すなら1コンポーネント単位で移す、という方針を先に決めます。
2つ目は、refとreactiveの誤解です。プリミティブな値や差し替えが多い配列はrefが扱いやすく、まとまったオブジェクトを深く更新する場合はreactiveも選択肢になります。ただし、分割代入でリアクティブ性が切れるケースがあるため、初心者はref中心で始める方が安全です。
3つ目は、propsを直接更新してしまうことです。親から渡された値を子が書き換えると、データの流れが読めなくなります。ローカル編集用のrefを作る、またはdefineModelやemitで親へ変更を返す方が保守しやすいです。
4つ目は、watchの乱用です。検索結果、件数、表示ページのような派生値はcomputedで表現します。watchはURLクエリの同期、ページ番号のリセット、外部API呼び出しのような副作用に限定します。
5つ目は、型がいつの間にかanyになることです。Claude Codeがエラーを避けるためにas anyを入れる場合があります。レビュー時はany、unknown、型アサーション、空配列の推論を必ず確認します。
6つ目は、SFCを細かく分割しすぎることです。再利用されない小さな部品を大量に作ると、propsとemitの配線だけが増えます。まずComposableでロジックを切り出し、UI部品は2回以上使う見込みがあるものから分けます。
7つ目は、テスト不足です。Claude Codeが作ったコードは見た目が整っていても、境界値やエラー時の状態が抜けやすいです。フォームなら短すぎる入力、送信中、送信成功、送信失敗、storeの状態変化を最低限確認します。
実務での進め方
実務では、1回の依頼で「画面全部を作って」と頼むより、小さなサイクルで進めます。最初に既存SFCを読ませて問題点を箇条書きにさせます。次に型だけ直します。その後、Composableへ抽出し、Pinia storeを整え、最後にテストを追加します。
レビューでは、Claude Codeの説明をそのまま信じず、差分を見ます。特にpackage.jsonの不要な追加、UI文言の勝手な変更、CSSの広範囲な変更、型エラーを隠すためのany、テストを通すためだけの実装変更を確認します。
チームでClaude CodeをVue開発に導入する場合は、CLAUDE.mdに「Vueはscript setupを標準にする」「storeはPinia setup storeを使う」「派生値はcomputed」「確認コマンドはnpm run typecheckとnpm run test:unit」のようなルールを置くと効果が出ます。ClaudeCodeLabでは、こうしたルール作り、既存Vueコードの棚卸し、Claude Codeの使い方トレーニングを相談・トレーニングで支援しています。
まとめ
Claude CodeはVue 3開発で、フォーム実装、Pinia状態管理、Composable抽出、テスト追加、既存Vue改修を速くします。一方で、Vueの柔軟さは落とし穴にもなります。Options APIとComposition APIを混ぜない、propsを直接更新しない、watchを乱用しない、anyを見逃さない、テストを足す。この基本を依頼文に含めるだけで、生成される差分の品質は大きく変わります。
初心者は、まず小さなフォームとstoreから始めてください。Claude Codeに一気に大規模な設計を任せるより、1つのSFC、1つのComposable、1つのstore、1つのテストを順番に整える方が、Vueの理解もコード品質も伸びます。
実際に試した結果
この記事の流れを小さなVue 3 + TypeScript検証プロジェクトで試したところ、Claude Codeに最初から「フォーム、store、テストを全部作って」と頼むより、SFC、Pinia、Composable、Vitestの順に分けた方がレビュー時間を短縮できました。特に効果が大きかったのは、依頼文に「propsを直接変更しない」「anyを使わない」「派生値はcomputed」を入れたことです。生成直後の差分で直す箇所が減り、npm run typecheckとnpm run test:unitで確認しやすい形になりました。
無料PDF: Claude Code はじめてのチートシート
まずは無料PDFで基本コマンドと最初の使い方をまとめて確認してください。登録後はそのままテンプレート集や導入相談にも進めます。
スパムは送りません。登録情報は厳重に管理します。
Claude Codeを仕事で使える形にしませんか?
無料PDFで基礎を固めたあと、すぐ使えるテンプレート集で試し、必要なら業務自動化や導入相談まで進められます。
この記事を書いた人
Masa
Claude Codeの実務活用、導入設計、収益導線改善を検証しているエンジニア。10言語の技術メディアを運営中。
関連書籍・参考図書
この記事のテーマに関連する書籍を楽天ブックスで探せます。
※ 当サイトは楽天市場のアフィリエイトプログラムに参加しています。上記リンクから商品をご購入いただくと、運営者に紹介料が支払われる場合があります。
関連記事
ObsidianメモをCLAUDE.mdに変えるClaude Code運用: 文脈を毎回説明しない仕組み
Obsidianの作業メモからCLAUDE.md用の運用ノートを作り、Claude Codeに安定した文脈を渡す方法。
Claude Code Revenue CTA Routing: 記事からPDF、Gumroad、相談へ送る設計
PVだけで終わらせず、読者の状態に合わせて無料PDF、Gumroad教材、導入相談へ分岐するCTA設計です。
Claude Codeチーム引き継ぎルール: レビュー、権限、収益導線まで渡す実務手順
Claude Codeの作業をチームで渡すための証拠、権限、ロールバック、無料PDF/Gumroad/相談導線の実務ルール。