Use Cases (Updated: 6/2/2026)

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 for Vue 3 Development: Practical TypeScript Guide

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.

SituationGood Claude Code taskHuman decision
Existing Vue refactorRead the SFC, find repeated logic, add typesUI compatibility and release risk
Form implementationCreate local state, validation, submit flowProduct wording and API contract
Pinia stateDefine store types, getters, actions, testsPersistence and ownership boundary
Composable extractionMove filtering and pagination logicWhether reuse is real enough
Test additionCover state transitions and regressionsRisk 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.

#Claude Code #Vue.js #Nuxt.js #Pinia #Composition API
Free

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.

Masa

About the Author

Masa

Engineer focused on practical Claude Code workflows. Runs claudecode-lab.com, a 10-language technical media site.