Use Cases (Actualizado: 2/6/2026)

Claude Code para Vue 3: guía práctica con TypeScript y Pinia

Flujo práctico para usar Claude Code con Vue 3, TypeScript, formularios, Pinia, composables y Vitest.

Claude Code para Vue 3: guía práctica con TypeScript y Pinia

Claude Code aporta más valor en Vue 3 cuando se usa como asistente que entiende el repositorio, no como simple generador de componentes. En un proyecto real, el trabajo importante suele ser leer un SFC existente, conservar los tipos de TypeScript, extraer lógica repetida a un composable, mover estado compartido a Pinia y añadir pruebas de Vitest antes de publicar.

Vue es flexible, y esa flexibilidad también crea riesgos. Un mismo proyecto puede mezclar Options API, Composition API, script setup, Pinia, viejas costumbres de Vuex, convenciones de Nuxt y helpers locales. Si el prompt solo dice “crea un formulario en Vue”, Claude Code puede generar algo que funciona, pero que no respeta las fronteras del proyecto.

Esta guía cubre cinco casos de uso con Claude Code + Vue 3 + TypeScript: refactorizar Vue existente, construir un formulario, diseñar estado con Pinia, extraer un composable y añadir pruebas con Vitest. Para la base técnica conviene consultar la guía oficial de Vue TypeScript con Composition API, la documentación de Pinia y la guía de Vitest. Para temas relacionados, revisa TypeScript tips y la guía de estrategia de testing.

Supuestos del proyecto

El flujo está pensado para aplicaciones con Vue 3, TypeScript, Vite, Pinia y Vitest. Encaja bien en paneles internos, mesas de ayuda, sistemas de reservas, aprobaciones internas y administración de contenido. En estos productos, la dificultad no está en una animación vistosa, sino en formularios, filtros, paginación, estado compartido y pruebas que deben evolucionar sin romperse.

Antes de pedir código, define un harness: una base de seguridad para que el agente trabaje. Ese harness incluye archivos permitidos, convenciones, prohibiciones, comandos de verificación y puntos de revisión. Sin esa estructura, Claude Code puede resolver el problema local añadiendo any, cambiando textos de UI o moviendo lógica a una capa incorrecta.

SituaciónTarea adecuada para Claude CodeDecisión humana
Refactor Vue existenteLeer SFC, detectar duplicación, reforzar tiposCompatibilidad visual y riesgo
FormularioEstado local, validación, envíoMensajes y contrato de API
PiniaTipos, getters, actions, testsPersistencia y límites
ComposableFiltros, paginación, reutilizaciónSi la reutilización es real
TestsCasos normales, errores, regresiónCobertura necesaria

Vista de arquitectura

Cuando los ejemplos crecen, la pregunta clave no es si Claude Code puede escribir el código. La pregunta es dónde debe vivir cada responsabilidad. Una frontera clara hace que el diff generado sea mucho más fácil de revisar.

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

El SFC se queda con la interfaz y los eventos del usuario. El composable contiene lógica reactiva reutilizable, como filtros y paginación. El store de Pinia guarda estado compartido. Vitest protege el comportamiento que no debe romperse.

Crear una base Vue 3 + TypeScript

Para una prueba nueva, empieza con el scaffolding oficial de Vue. En un repositorio existente, primero confirma que el typecheck y las pruebas actuales pasan; así podrás evaluar si el cambio de Claude Code introduce problemas.

npm create vue@latest support-desk-vue -- --typescript --router --pinia --vitest
cd support-desk-vue
npm install
npm run dev

No escribas solo “haz una app moderna en Vue”. Indica si se usa npm, pnpm o yarn, si existe Vue Router, si Pinia es obligatorio y qué comando valida el cambio. En una app real, pide a Claude Code que lea package.json, vite.config.ts, tsconfig.app.json, src/components y src/stores antes de editar.

Caso 1: formulario SFC con tipos

Un formulario es un buen primer caso porque reúne v-model, props, emit, validación, estado de guardado y actions de store. El ejemplo puede vivir en src/components/TicketForm.vue. No muta props; usa ref local para los valores editables y emite el id cuando el store termina de guardar.

<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>

Al pedir este componente, añade restricciones: no mutar props, no introducir any, no añadir una librería visual y no mezclar detalles de API dentro del SFC. Esas restricciones evitan mucho trabajo de limpieza.

Caso 2: estado compartido con Pinia

Si varias pantallas usan la lista de tickets, el formulario no debe poseer todo el estado. Pinia permite concentrar la lista, el indicador de guardado, los getters y las acciones en una capa clara.

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,
  };
});

En producción, saveTicket llamaría a un cliente de API tipado. Aun así, conviene pedir a Claude Code que primero estabilice el modelo del store, después conecte la red y finalmente agregue manejo de errores. Así cada revisión queda acotada.

Caso 3: extraer un composable

Un composable es una función reutilizable que contiene lógica reactiva de Vue. En palabras simples, es la parte lógica de la pantalla separada de la pantalla. Filtros, prioridad seleccionada y paginación son candidatos claros.

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,
  };
}

El watch solo reinicia la página cuando cambian los filtros. Los valores derivados se quedan en computed. Esta regla debe aparecer en el prompt: usa computed para derivar y watch solo para efectos secundarios explícitos.

Caso 4: pruebas con Vitest

Cuando la implementación se acelera, las pruebas de regresión valen más. Empezar por el store es práctico porque está cerca del comportamiento de negocio y no depende del DOM.

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');
  });
});

No pidas simplemente “más tests”. Pide comportamientos: entrada demasiado corta, entrada válida, estado de guardado, éxito, fallo y cambio de estado en Pinia. Eso produce pruebas que protegen trabajo real.

Prompt para refactorizar Vue existente

En código existente, evita una reescritura enorme. Primero haz que Claude Code lea el componente y enumere riesgos; luego refuerza tipos, extrae un composable, ajusta Pinia si hace falta y añade pruebas.

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.

Este prompt separa lo que Claude Code puede decidir de lo que debe validar el equipo. La extracción, los tipos y el borrador de pruebas son buen trabajo para la herramienta. Los textos, la accesibilidad, el contrato de API y el momento de release deben revisarse por personas.

Errores frecuentes

El primer error es mezclar Options API y Composition API sin plan. Vue 3 soporta ambas, pero un componente con data, methods, setup y script setup a la vez se vuelve difícil de revisar.

El segundo es confundir ref y reactive. Para valores primitivos, arrays y datos que se reemplazan, ref suele ser más fácil. reactive sirve para objetos cohesivos, pero la destructuración puede sorprender.

El tercero es mutar props. Un hijo no debería cambiar silenciosamente estado del padre. Usa ref local, emit hacia el padre o defineModel si el equipo ya acordó ese patrón.

El cuarto es abusar de watch. Si un valor se deriva, usa computed. Deja watch para sincronizar rutas, reiniciar páginas, llamar APIs o escribir en almacenamiento.

El quinto es permitir que los tipos caigan en any. Claude Code puede usar as any para pasar un error. Revisa any, unknown, aserciones y arrays vacíos antes de aceptar.

El sexto es partir el SFC en demasiados componentes. Muchas piezas pequeñas sin reutilización real añaden props y emits. Extrae primero lógica a composables y divide UI solo cuando tenga sentido.

El séptimo es la falta de pruebas. Código generado puede verse correcto y fallar en bordes. En formularios, cubre entradas cortas, envío válido, guardado, éxito, fallo y cambios del store.

Flujo práctico y CTA

En un repositorio real, no empieces con “construye toda la pantalla”. Pide primero una lectura de riesgos, luego un cambio pequeño, ejecuta typecheck y tests, y continúa. Diffs pequeños producen mejores revisiones.

Para equipos, documenta reglas en CLAUDE.md: Vue con script setup, estado compartido en Pinia setup stores, derivados con computed, efectos con watch, y verificación con npm run typecheck y npm run test:unit. ClaudeCodeLab puede ayudar a convertir estas reglas en un flujo repetible mediante formación y consultoría Claude Code.

Resumen

Claude Code acelera refactors de Vue 3, formularios, Pinia, composables y pruebas. La diferencia entre un buen diff y una deuda nueva está en el límite del prompt: archivos permitidos, estilo Vue, cosas que no deben cambiar y comandos de verificación.

Si estás empezando, trabaja con un SFC, un composable, un store y un archivo de test. Ese ciclo pequeño enseña el modelo mental de Vue y produce código cercano a producción.

Resultado al probarlo

Probé este flujo en un proyecto pequeño con Vue 3 + TypeScript. Pedir a Claude Code formulario, store, composable y tests en una sola solicitud generó un diff grande y más lento de revisar. Separar el trabajo en SFC, Pinia, composable y Vitest hizo que los problemas fueran más fáciles de detectar. Las restricciones más útiles fueron “no mutar props”, “no introducir any” y “usar computed para valores derivados”. El resultado se validó mejor con npm run typecheck y npm run test:unit.

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

PDF gratis: cheatsheet de Claude Code

Introduce tu email y descarga una hoja con comandos, hábitos de revisión y flujos seguros.

Cuidamos tus datos y no enviamos spam.

Masa

Sobre el autor

Masa

Ingeniero enfocado en workflows prácticos con Claude Code.