Modernizar código legado com Claude Code com segurança
Workflow prático para modernizar código legado com Claude Code: testes, TypeScript, riscos, exemplos executáveis e verificação.
Modernização falha quando começa grande demais
Código legado não é apenas código antigo. É código cujo comportamento é difícil de provar, com poucos testes, regras de negócio implícitas e risco real para pedidos, cobrança, suporte ou publicação. Claude Code ajuda muito nesse cenário, mas não deve ser usado como botão de reescrita em massa.
O caminho seguro é investigar primeiro, travar o comportamento atual com testes depois, e só então refatorar em passos pequenos. Testes de caracterização registram como o sistema funciona hoje, mesmo que esse comportamento não seja o ideal. Harness, aqui, é o andaime que permite o trabalho do agente: testes, permissões, comandos, regras do projeto e checklist de revisão.
A documentação oficial de Claude Code common workflows cobre exploração de codebase, refatoração, testes, PRs e worktrees. Essa é a mentalidade correta para código legado: Claude Code aumenta a velocidade, mas o time continua responsável por risco, prioridade e aceite.
Três casos de uso práticos
Não escolha a prioridade só porque um arquivo parece feio. Comece pelas áreas em que reduzir incerteza reduz risco de negócio.
| Caso de uso | Objetivo | O que Claude Code pode fazer | O que humanos devem revisar |
|---|---|---|---|
| Pedidos, cobrança ou pagamento | Evitar erro de dinheiro e status do cliente | Mapear comportamento, adicionar testes, achar limites | Impostos, descontos, arredondamento, regras legais |
| Migração de JavaScript para TypeScript | Tornar mudanças futuras mais seguras | Adicionar tipos, reduzir any, corrigir erros em lotes | Compatibilidade da API pública |
| Callbacks ou funções gigantes | Melhorar manutenção sem alterar comportamento | Separar responsabilidades, sugerir nomes, explicar diffs | Erros, retries, efeitos colaterais e logs |
No ClaudeCodeLab, uso o mesmo fluxo para scripts antigos de publicação, integrações de checkout e transformações de conteúdo. O ponto principal não é Claude Code escrever rápido. O ponto principal é cada mudança ser pequena o suficiente para revisão e ligada a uma prova.
flowchart LR
A[Explorar] --> B[Travar comportamento com testes]
B --> C[Refatorar em passos pequenos]
C --> D[Adicionar tipos e organizar dependências]
D --> E[Revisão humana dos riscos]
E --> B
Comece com uma auditoria somente leitura
O primeiro prompt deve proibir edições. Nessa fase, você precisa de um mapa do sistema, não de um patch baseado em contexto incompleto.
Leia @src/legacy e @test.
Ainda não modifique arquivos.
Retorne esta auditoria:
1. Arquivos principais e responsabilidades
2. I/O externo, banco de dados, APIs, escrita em arquivos e efeitos colaterais
3. Comportamentos que precisam continuar compatíveis
4. Testes ausentes e ramos de alto risco
5. Ordem mais segura para mudanças pequenas
Se uma regra não estiver clara, escreva "precisa de confirmação humana" em vez de adivinhar.
A página oficial How Claude Code works explica que Claude Code pode ler arquivos, executar comandos e editar código. Esse poder é útil, mas em sistemas antigos a primeira fase deve ficar restrita a entendimento. Uma melhoria aparentemente idiomática pode alterar uma resposta usada por outro serviço.
Exemplo mínimo executável
O exemplo abaixo é pequeno, mas segue a mesma sequência que uso em trabalho real com legado.
mkdir legacy-modernization-demo
cd legacy-modernization-demo
npm init -y
npm install -D vitest typescript @types/node
npm pkg set type="module"
npm pkg set scripts.test="vitest run"
npm pkg set scripts.typecheck="tsc --noEmit"
mkdir -p src/legacy test
O processador antigo mistura validação, cálculo e montagem da resposta em um arquivo. Não é um exemplo absurdamente ruim; é o tipo de código que funciona e, por isso, fica anos sem ser tocado.
// src/legacy/orderProcessor.js
export function processOrder(order) {
if (!order || !Array.isArray(order.items) || order.items.length === 0) {
return { status: "error", message: "items is required" };
}
const subtotal = order.items.reduce((sum, item) => {
return sum + item.price * item.qty;
}, 0);
const discount = order.customer?.type === "vip" ? subtotal * 0.1 : 0;
return {
status: "confirmed",
total: subtotal - discount,
items: order.items,
discount
};
}
Agora trave o comportamento atual com Vitest. Esses testes não redesenham o domínio; eles protegem contratos que não podem mudar sem decisão explícita.
// test/orderProcessor.test.ts
import { describe, expect, it } from "vitest";
import { processOrder } from "../src/legacy/orderProcessor.js";
describe("processOrder legacy behavior", () => {
it("calculates total for a regular customer", () => {
const result = processOrder({
items: [
{ id: "A1", qty: 2, price: 1000 },
{ id: "B2", qty: 1, price: 500 }
],
customer: { id: "C1", type: "regular" }
});
expect(result).toMatchObject({
status: "confirmed",
total: 2500,
discount: 0
});
});
it("applies a 10 percent VIP discount", () => {
const result = processOrder({
items: [{ id: "A1", qty: 1, price: 10000 }],
customer: { id: "C2", type: "vip" }
});
expect(result.status).toBe("confirmed");
expect(result.total).toBe(9000);
expect(result.discount).toBe(1000);
});
it("returns an error when items are empty", () => {
const result = processOrder({
items: [],
customer: { id: "C3", type: "regular" }
});
expect(result.status).toBe("error");
expect(result.message).toContain("items");
});
});
Execute npm test. Só depois de passar, peça para Claude Code editar.
Leia @src/legacy/orderProcessor.js e @test/orderProcessor.test.ts.
Migre este código para TypeScript mantendo os testes verdes.
Regras:
- Mantenha o nome público processOrder
- Preserve status, total, discount e message
- Adicione tipos primeiro e depois separe responsabilidades
- Execute npm test e npm run typecheck
- Explique que compatibilidade cada diff preservou
Uma estrutura TypeScript mais revisável
Depois da modernização, tipos, validação, cálculo e orquestração ficam separados. A meta não é abstração bonita, e sim lógica de negócio mais fácil de revisar.
// src/orderTypes.ts
export type CustomerType = "regular" | "vip";
export type OrderItem = {
id: string;
qty: number;
price: number;
};
export type OrderInput = {
items: OrderItem[];
customer: {
id: string;
type: CustomerType;
};
};
export type OrderResult =
| {
status: "confirmed";
total: number;
items: OrderItem[];
discount: number;
}
| {
status: "error";
message: string;
};
// src/validators.ts
import type { OrderInput } from "./orderTypes";
export function validateOrder(order: OrderInput | null | undefined): string | null {
if (!order || !Array.isArray(order.items) || order.items.length === 0) {
return "items is required";
}
return null;
}
// src/calculators.ts
import type { CustomerType, OrderItem } from "./orderTypes";
export function calculateSubtotal(items: OrderItem[]): number {
return items.reduce((sum, item) => sum + item.price * item.qty, 0);
}
export function calculateDiscount(subtotal: number, customerType: CustomerType): number {
return customerType === "vip" ? subtotal * 0.1 : 0;
}
// src/orderProcessor.ts
import { calculateDiscount, calculateSubtotal } from "./calculators";
import type { OrderInput, OrderResult } from "./orderTypes";
import { validateOrder } from "./validators";
export function processOrder(order: OrderInput): OrderResult {
const validationMessage = validateOrder(order);
if (validationMessage) {
return { status: "error", message: validationMessage };
}
const subtotal = calculateSubtotal(order.items);
const discount = calculateDiscount(subtotal, order.customer.type);
return {
status: "confirmed",
total: subtotal - discount,
items: order.items,
discount
};
}
Altere o import do teste para ../src/orderProcessor, depois rode npm test e npm run typecheck. Um diff desse tamanho ainda é fácil de revisar. Se o mesmo PR mover pastas, atualizar dependências, trocar formatação e renomear conceitos de domínio, a revisão perde força.
Separe atualizações de dependências
Outro erro comum é misturar refatoração com upgrades major. Quando os testes quebram, fica difícil saber se a causa é TypeScript, uma mudança de API, o bundler ou sua própria lógica.
Peça primeiro um inventário.
Leia package.json e o lockfile.
Não atualize nada ainda.
Retorne uma tabela com:
- package
- versão atual
- versão alvo recomendada
- se é major upgrade
- URL do guia oficial de migração
- arquivos provavelmente afetados
- testes que devemos adicionar antes da atualização
Para operações destrutivas ou amplas, mantenha permissões conservadoras. A documentação de Claude Code permissions vale ser lida antes de autorizar migrações, exclusões ou deploys. Velocidade do agente não ajuda se ela remove a etapa de aprovação que protegia o sistema.
Armadilhas concretas
A primeira armadilha é refatorar sem testes. Código mais limpo ainda é regressão se arredondamento, desconto ou mensagem de erro mudarem.
A segunda é aceitar sugestão de Claude Code como regra de negócio. Um retorno mais idiomático não significa que clientes antigos conseguem consumi-lo.
A terceira é criar um PR gigante. Migração de tipos, separação de lógica, atualização de dependências, movimentação de arquivos e formatação devem ser separados quando possível.
A quarta é melhorar tratamento de erros rápido demais. Em sistemas antigos, um null, uma string específica ou um status HTTP estranho pode ser contrato.
A quinta é deixar documentação para o fim. Peça para Claude Code escrever notas de compatibilidade, passos de verificação manual e plano de rollback na descrição do PR.
Revisão, links e CTA
Combine este fluxo com o guia de automação de refatoração, TDD com Claude Code e geração automática de documentação. Para times, coloque áreas proibidas, comandos de teste, termos de domínio e regras de revisão em boas práticas de CLAUDE.md.
ClaudeCodeLab ajuda equipes a introduzir Claude Code em produtos existentes com treinamento, templates CLAUDE.md e consultoria de modernização. O objetivo não é reescrita por IA, mas um fluxo repetível em que testes, permissões e revisão humana trabalham juntos.
Resultado verificado
Testei o fluxo criando legacy-modernization-demo: processador JavaScript legado, três comportamentos travados com Vitest, migração para TypeScript e execução de npm test com npm run typecheck. O maior ganho veio de separar auditoria somente leitura e prompt de edição. Os diffs ficaram menores, e foi mais fácil verificar total, desconto VIP e erro de pedido vazio.
Artigos relacionados
Guia de colaboração em equipe com Claude Code: handoff, revisão e permissões
Fluxo prático de Claude Code para CLAUDE.md, permissões, revisão de PR, handoff e onboarding.
Desenvolvimento Rust com Claude Code: Cargo, ownership, testes e CLI
Guia prático de Rust com Claude Code: Cargo, ownership, testes, fmt, clippy, erros e um CLI pequeno copiável.
Primeiro dia com Claude Code num repositório alheio: como escolher a 1ª tarefa sem quebrar nada
Primeiro dia com Claude Code num código alheio: ordem de leitura, zonas intocáveis, primeira tarefa pequena e prova, tudo em 30 minutos.
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.