Claude Code para Vue 3: guia prático com TypeScript e Pinia
Use Claude Code com Vue 3, TypeScript, formulários, Pinia, composables e Vitest em fluxo real de projeto.
Claude Code traz mais valor em Vue 3 quando é usado para trabalhar dentro de um repositório real, não apenas para gerar um componente isolado. Em projetos de produção, o trabalho importante costuma ser ler um SFC existente, preservar os tipos TypeScript, extrair lógica repetida para um composable, mover estado compartilhado para Pinia e adicionar testes Vitest antes de publicar.
Vue é flexível, e essa flexibilidade também cria riscos. Um projeto pode misturar Options API, Composition API, script setup, Pinia, hábitos antigos de Vuex, convenções de Nuxt e helpers locais. Se o prompt for apenas “crie um formulário em Vue”, Claude Code pode entregar algo que funciona, mas que não respeita a arquitetura do time.
Este guia mostra Claude Code + Vue 3 + TypeScript em cinco situações práticas: melhorar Vue existente, implementar formulário, estruturar estado com Pinia, extrair composable e adicionar testes com Vitest. Para base técnica, consulte a documentação oficial de Vue TypeScript com Composition API, Pinia e Vitest. Como leitura complementar, veja TypeScript tips e estratégia de testes.
Premissas do projeto
O fluxo considera Vue 3, TypeScript, Vite, Pinia e Vitest. Ele combina com painéis internos, help desks, sistemas de reserva, aprovações internas e áreas administrativas. Nesses produtos, a complexidade aparece em formulários, listas, filtros, paginação, estado compartilhado e testes.
Antes de pedir código, dê a Claude Code um harness, ou seja, uma base segura para o agente trabalhar. Esse harness inclui arquivos permitidos, convenções, proibições, comandos de verificação e pontos de revisão. Sem isso, o agente pode resolver o problema local adicionando any, alterando textos da UI ou movendo lógica para a camada errada.
| Situação | Tarefa boa para Claude Code | Decisão humana |
|---|---|---|
| Refatorar Vue existente | Ler SFC, achar duplicação, reforçar tipos | Compatibilidade visual e risco |
| Formulário | Estado local, validação, envio | Mensagens e contrato de API |
| Pinia | Tipos, getters, actions, testes | Persistência e limites |
| Composable | Filtros, paginação, reutilização | Se há reutilização real |
| Testes | Casos normais, erros, regressão | Nível de cobertura |
Visão de arquitetura
Quando o exemplo cresce, a pergunta principal não é se Claude Code consegue escrever o código. A pergunta é onde cada responsabilidade deve ficar. Fronteiras claras tornam o diff gerado muito mais 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
O SFC fica com a tela e os eventos do usuário. O composable contém lógica reativa reutilizável, como filtros e paginação. O store Pinia guarda estado compartilhado. Vitest protege o comportamento que não pode quebrar.
Criar a base Vue 3 + TypeScript
Para um experimento novo, comece pelo scaffolding oficial de Vue. Em um repositório existente, confirme primeiro que typecheck e testes passam; assim você avalia o impacto de Claude Code sobre uma base limpa.
npm create vue@latest support-desk-vue -- --typescript --router --pinia --vitest
cd support-desk-vue
npm install
npm run dev
Não escreva apenas “faça um app Vue moderno”. Informe gerenciador de pacotes, Vue Router, Pinia e comando de teste. Em uma aplicação real, peça para Claude Code ler package.json, vite.config.ts, tsconfig.app.json, src/components e src/stores antes de editar.
Caso 1: formulário SFC tipado
Formulários são bons para começar porque juntam v-model, props, emit, validação, estado de salvamento e actions do store. O exemplo abaixo pode ficar em src/components/TicketForm.vue. Ele não muta props, mantém valores editáveis em refs locais e emite o id após salvar.
<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>
Ao pedir esse componente, inclua restrições: não mutar props, não introduzir any, não adicionar biblioteca visual e não colocar detalhes de API no SFC. Restrições negativas evitam muita limpeza manual.
Caso 2: estado compartilhado com Pinia
Se várias telas usam a lista de tickets, o formulário não deve possuir todo o estado. Pinia concentra lista, flag de salvamento, getters e actions em uma camada 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,
};
});
Em produção, saveTicket chamaria um cliente de API tipado. Mesmo assim, revise primeiro o modelo do store, depois conecte rede e por fim adicione tratamento de erro. Cada diff fica menor.
Caso 3: extrair um composable
Composable é uma função reutilizável com lógica reativa de Vue. Em termos simples, é a lógica da tela separada da tela. Busca, filtro por prioridade e paginação são bons candidatos.
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,
};
}
Aqui watch só reseta a página quando os filtros mudam. Valores derivados ficam em computed. Essa regra deve entrar no prompt: computed para derivação, watch para efeitos explícitos.
Caso 4: testes com Vitest
Quanto mais rápida fica a implementação, mais importantes são os testes de regressão. Comece pelo store, pois ele está perto do comportamento de negócio e é menos frágil que testes de 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');
});
});
Não peça apenas “adicione mais testes”. Peça comportamentos: entrada curta, entrada válida, estado de salvamento, sucesso, falha e mudança no estado Pinia.
Prompt para refatorar Vue existente
Em código existente, evite uma grande reescrita. Primeiro peça leitura e riscos, depois ajuste tipos, extraia um composable, organize Pinia se necessário e adicione testes.
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.
Esse prompt separa o que Claude Code pode propor do que o time precisa decidir. Extração, tipos e testes são bons trabalhos para a ferramenta. Texto de UI, contrato de API, acessibilidade e timing de release precisam de revisão humana.
Armadilhas comuns
A primeira armadilha é misturar Options API e Composition API sem plano. Vue 3 suporta as duas, mas um componente com data, methods, setup e script setup fica difícil de revisar.
A segunda é entender mal ref e reactive. Para valores primitivos, arrays e dados substituíveis, ref costuma ser mais simples. reactive funciona bem para objetos coesos, mas destructuring pode confundir.
A terceira é mutar props. Um filho não deve alterar silenciosamente estado do pai. Use ref local, emit ou defineModel quando o time já tiver adotado esse padrão.
A quarta é abusar de watch. Se um valor pode ser derivado, use computed. Guarde watch para sincronizar rota, resetar página, chamar API ou escrever em storage.
A quinta é deixar tipos caírem em any. Claude Code pode usar as any para contornar erro. Revise any, unknown, assertions e inferência de array vazio.
A sexta é dividir SFC demais. Muitos componentes pequenos sem reutilização real aumentam props e emits. Extraia lógica para composables primeiro e só divida UI quando houver independência.
A sétima é falta de teste. Código gerado pode parecer limpo e falhar em bordas. Em formulários, cubra entrada curta, envio válido, salvamento, sucesso, falha e mudança no store.
Fluxo prático e CTA
Em um repositório real, não comece com “construa a tela inteira”. Peça análise de riscos, faça uma mudança pequena, rode typecheck e testes, depois avance. Diffs pequenos melhoram a revisão.
Para equipes, documente regras em CLAUDE.md: Vue com script setup, estado compartilhado em Pinia setup stores, derivados em computed, efeitos em watch, verificação com npm run typecheck e npm run test:unit. ClaudeCodeLab pode ajudar a transformar isso em processo repetível por meio de treinamento e consultoria Claude Code.
Resumo
Claude Code acelera refatorações Vue 3, formulários, Pinia, composables e testes. A qualidade depende do limite do prompt: arquivos permitidos, estilo Vue, o que não pode mudar e comandos de verificação.
Quem está começando deve trabalhar com um SFC, um composable, um store e um arquivo de teste. Esse ciclo pequeno ensina o modelo mental do Vue e gera código próximo de produção.
Resultado ao testar
Testei esse fluxo em um projeto pequeno com Vue 3 + TypeScript. Pedir formulário, store, composable e testes em uma única solicitação gerou um diff grande e mais demorado de revisar. Separar em SFC, Pinia, composable e Vitest facilitou a inspeção. As restrições mais úteis foram “não mutar props”, “não introduzir any” e “usar computed para valores derivados”. O resultado ficou mais fácil de validar com npm run typecheck e npm run test:unit.
Artigos relacionados
Desenvolvimento de APIs de produção com Claude Code: OpenAPI, Next.js, Zod e CI
Crie APIs de produção com Claude Code: contrato OpenAPI, Route Handlers do Next.js, Zod, testes e CI.
Desenvolver ferramentas CLI com Claude Code: Node.js, TypeScript, testes e releases seguros
Crie uma CLI Node.js/TypeScript com Claude Code: argumentos, config, stdin/stdout, exit codes, testes, CI e segredos.
Rate limiting de APIs com Claude Code: 429, Redis e Cloudflare
Implemente rate limiting de APIs com Claude Code: 429, Redis, Cloudflare, prevenção de abuso e erros comuns.
PDF grátis: cheatsheet do Claude Code
Informe seu e-mail e baixe uma página com comandos, hábitos de revisão e workflows seguros.
Cuidamos dos seus dados e não enviamos spam.
Sobre o autor
Masa
Engenheiro focado em workflows práticos com Claude Code.