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.
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.
Artigos relacionados
Workflow Obsidian para CLAUDE.md com Claude Code
Transforme notas de trabalho do Obsidian em notas operacionais CLAUDE.md para preservar contexto.
Claude Code Revenue CTA Routing: artigos para PDF, Gumroad e consultoria
Um fluxo com Claude Code para levar leitores ao PDF grátis, Gumroad ou consultoria conforme intenção.
Regras de handoff para equipes com Claude Code: evidências, permissões, rollback e receita
Formato prático para entregar trabalho do Claude Code com prova, permissões, rollback, PDF grátis, Gumroad e consultoria.