Como migrei uma API gigantesca de Fastify para NestJS usando Claude Code

March 22, 2026

Como migrei uma API gigantesca de Fastify para NestJS usando Claude Code

O repositório tinha um arquivo chamado http/router.ts com 3033 linhas. Um god file. Cada novo endpoint era mais uma linha nesse labirinto, e onboarding de novos devs tinha virado uma conversa de horas antes de qualquer PR.

Esse post é sobre como decidi migrar do zero para NestJS, a estratégia que usei para não derrubar nada em produção, e como o Claude Code executou boa parte disso enquanto eu dormia.

O número que vale destacar antes de qualquer coisa: 16 módulos, 79+ rotas, migração completa em 1 hora e 59 minutos.

23:32 chore: initial NestJS scaffolding 23:34 chore: update schema and auth to match domain 23:58 chore: add dependencies and env vars 00:05 feat: @Public decorator + global JWT guard (task 2) 00:14 feat: shared module - S3, PDF, BullMQ, templates (task 3) 00:23 feat: auth module - register, Google OAuth, /me (task 4) 00:31 feat: users module - profile, onboarding, photo (task 5) 00:56 feat: professions + councils modules (task 6) 00:54 feat: materials module - CRUD, favorites, access (task 7) 01:01 feat: patients module (task 8) 01:05 feat: assessments module - triage routes (task 9) 01:06 feat: formulas + custom-formulas modules (task 10) 01:11 feat: forms module - rules, payload builder (task 11) 01:18 feat: prescriptions - PDF, public HTML view (task 12) 01:23 feat: prescription-configurations - image uploads (task 13) 01:26 feat: webhooks module - Resend events, cron (task 15) 01:27 feat: certillion module - digital signature (task 14) 01:31 feat: admin module - dashboard, menopause analytics (task 16)

Por que migrar

O stack original era Fastify 5 + Bun. Rápido na teoria, problemático na prática.

O god file era o sintoma mais visível, mas não o único. Toda rota da API estava num único router.ts. Adicionar um endpoint novo significava navegar por 3000 linhas, entender onde encaixar, torcer para não quebrar nada.

A DI com InversifyJS existia, mas sem integração estruturada com o Fastify. O resultado era um híbrido: parte do código usava DI, parte instanciava direto. Inconsistência que escalou para todo o time.

O runtime Bun foi o problema que mais custou tempo. Puppeteer para PDF, bcrypt para senhas, ambos com incompatibilidades. Cada atualização de dependência virava uma roleta.

A ausência de convenção deixava guards, interceptors e pipes implementados na mão, cada dev de um jeito diferente.

A troca para NestJS não era de framework, era de paradigma. Ele resolve tudo isso nativamente, e mais importante: resolve de um jeito que qualquer dev já conhece.

A estratégia: repositório paralelo com corte gradual

A alternativa óbvia seria um big bang: pausar tudo, migrar, deployar. Mas com uma API em produção e time usando, isso era inviável. Downtime real, risco real.

A estratégia foi outra: repositório novo, rodando em paralelo, com o Fastify intacto em produção enquanto o NestJS era construído e validado.

O Fastify ficaria em produção durante toda a migração. O NestJS subiria numa porta separada, apontando para o mesmo banco, o mesmo schema Prisma, sem migração de dados.

O plano original era usar o Nginx para redirecionar tráfego módulo a módulo conforme cada parte fosse validada em produção. Na prática, ficou mais simples do que isso.

Como o corte de tráfego aconteceu de verdade

Depois de construir a API completa, optei por uma abordagem mais direta: atualizar os endpoints no frontend para apontar para a nova URL do NestJS. Nada de Nginx, nada de roteamento parcial.

A razão é simples. O Nginx gradual faz sentido quando você quer colocar módulos em produção antes de ter a API completa, isolando o risco. Mas quando a API inteira está pronta e revisada, o corte total via frontend é mais limpo e força um teste real de tudo ao mesmo tempo. Qualquer problema fica imediatamente visível, não espalhado em semanas de rollouts parciais.

O rollback, se necessário, seria trocar a URL de volta. Mais direto impossível.

A estrutura do projeto NestJS

Definir a estrutura antes de qualquer código foi a decisão mais importante da migração. Não a mais técnica, a mais importante.

Cada módulo segue o mesmo padrão sem exceção:

modules/professions/ ├── professions.module.ts ├── professions.controller.ts ├── professions.service.ts ├── professions.repository.ts └── dto/ ├── create-profession.dto.ts └── update-profession.dto.ts

Controller recebe HTTP e delega. Service guarda a lógica de negócio. Repository isola as queries Prisma. DTO define os contratos com class-validator. Parece burocrático no início; no mês 3 é o que mantém o projeto navegável.

Como o Claude Code executou a migração

Skills do Superpowers: o que mudou o jogo

O Claude Code sozinho é capaz. Com as skills do Superpowers instaladas, ele vira um processo de engenharia estruturado.

As que fizeram diferença nessa migração:

brainstorming abre um diálogo antes de qualquer código para definir arquitetura, trade-offs e decisões. Foi aqui que a estratégia de repositório paralelo foi pensada e validada.

writing-plans gera um plano detalhado com arquivos, passos e código. Esse plano virou o contrato que todos os agentes seguiram. Sem ele, cada agente teria interpretado a tarefa do seu jeito e o output seria inconsistente.

subagent-driven-development orquestra a execução com um agente por tarefa, review entre cada etapa, feedback loop antes do próximo começar. É o que tornou possível 16 módulos revisados em menos de 2 horas.

using-git-worktrees configura worktrees isolados para cada tarefa, o que permite execução paralela real. Dois módulos sendo implementados ao mesmo tempo, em branches separados, sem conflito.

code-reviewer é um agente especializado em revisar o output de cada tarefa contra o plano original. Foi ele que pegou bugs reais antes de ir para produção.

O fluxo completo:

A diferença entre usar Claude Code com e sem essa estrutura é a diferença entre gerar código e executar engenharia de fato.

O plano como contrato

Antes de qualquer código, documentei um plano completo: estrutura de pastas, padrão de cada módulo, ordem de execução, e qual código do Fastify correspondia a qual módulo do NestJS. Esse documento virou a referência de todos os agentes.

O Claude Code não adivinha contexto. Quanto mais preciso o plano, menos re-trabalho.

Worktrees para execução paralela

Cada módulo foi implementado num git worktree separado, cada um num branch isolado:

prescrevamais-backend/ └── .worktrees/ ├── feat-prescriptions-module/ ├── feat-prescription-configs-module/ ├── feat-certillion-module/ └── feat-webhooks-module/

Enquanto um agente implementava PrescriptionsModule, outro estava no PrescriptionConfigurationsModule. Ao terminar, review em paralelo, fixes, merge.

O ciclo de cada módulo

1. Agente implementa no worktree 2. Code reviewer analisa o output 3. Fixes aplicados no mesmo worktree 4. Merge para main 5. Worktree removido

O code reviewer pegou problemas reais. Alguns exemplos do que foi encontrado e corrigido antes de qualquer merge:

Prescription-configurations: setDefault fazia dois updates separados. O fix foi uma transação atômica, porque dois updates independentes abrem espaço para race condition e você acaba com dois registros isDefault: true ao mesmo tempo.

setDefaultTransactional(id: string, prescriberId: number) { return this.prisma.$transaction([ this.prisma.prescriptionConfiguration.updateMany({ where: { prescriberId }, data: { isDefault: false }, }), this.prisma.prescriptionConfiguration.update({ where: { id }, data: { isDefault: true }, }), ]); }

Admin module: getGlobalMetrics rodava 4 queries SQL em sequência. O reviewer identificou e reescreveu com Promise.all, reduzindo o tempo de resposta do endpoint pela metade.

Certillion module: signPrescription recebia accessToken e psc como @Body() raw strings sem validação. O reviewer adicionou o DTO correto com class-validator antes de qualquer merge.

O que o Claude Code fez bem

Converter Zod schemas para decorators class-validator com precisão foi onde mais poupou tempo. Também foi consistente gerando DTOs com decorators Swagger a partir de tipos TypeScript existentes, identificando onde lógica de negócio estava misturada com parsing de request no Fastify, e resolvendo merge conflicts em app.module.ts quando dois branches adicionavam imports no mesmo lugar.

Onde foi necessário supervisão

Lógica com regras implícitas que não estavam documentadas no código original foi o ponto mais frágil. O Claude reproduzia o que via; se o Fastify tinha um bug, o NestJS tinha o mesmo. Relacionamentos Prisma complexos às vezes vinham com n+1 que precisavam ser reescritos. E a ordem de migração em si nunca foi delegada: decidir qual módulo migrar primeiro é humano, não do agente.

A ordem de migração importa

Migrar na ordem errada cria dependências quebradas no NestJS enquanto partes do sistema ainda estão no Fastify. A ordem que funcionou:

Wave A: Fundação. Nenhum tráfego real ainda. Setup do NestJS, ConfigModule com validação Zod das variáveis de ambiente, PrismaModule global, AuthModule com JWT e Google OAuth. Essa base precisa estar sólida antes de qualquer módulo de negócio.

Wave B: Módulos simples. Poucos relacionamentos, lógica direta. Os primeiros módulos a entrar em produção, os mais seguros para validar o pipeline de deploy.

Wave C: Módulos com estado. Uploads de arquivo, relacionamentos mais complexos, lógica mais rica. Aqui começa a execução paralela de dois ou três módulos ao mesmo tempo.

Wave D: Core crítico. Prescrições, configurações do prescritor, geração de PDF. Só entra em produção depois de validação extensiva com dados reais. Não tem atalho aqui.

Wave E: Integrações externas e admin. Assinatura digital, webhooks, dashboards analíticos. Migrar por último reduz a janela de inconsistência entre os dois sistemas.

Os riscos que aprendi

Comportamento implícito no Fastify. O código original tinha lógica espalhada em middlewares, hooks e plugins que não eram óbvios ao ler o router. A migração revelou comportamentos que eu não sabia que existiam. Vale mapear todos os hooks antes de migrar qualquer módulo.

Validação assimétrica. O Fastify usava Zod; o NestJS usa class-validator. Pareciam equivalentes, mas havia diferenças sutis em campos opcionais e tipos union. Teste os DTOs com payloads de produção reais, não só com dados de teste.

O banco compartilhado amplifica bugs. Durante a migração, Fastify e NestJS escrevem no mesmo banco. Um bug de escrita no NestJS aparece para os dois. As primeiras semanas de cada módulo novo pedem atenção dobrada nas queries de escrita.

Duas APIs, duas configurações. Parece trivial até a primeira vez que uma variável de ambiente é atualizada num serviço e esquecida no outro. Automatize a sincronização desde o início.

O que o NestJS resolveu de verdade

Um desenvolvedor novo consegue entender um módulo inteiro lendo quatro arquivos em sequência. Antes, entender qualquer coisa exigia navegar por 3000 linhas sem mapa.

O Swagger gerado automaticamente pelos decorators nos DTOs acabou com uma categoria inteira de divergência entre documentação e implementação. A documentação não consegue ficar errada porque ela é o código.

Os guards globais com @Public() fecharam um padrão problemático: rotas que deveriam ser autenticadas mas não eram, porque autenticação no Fastify era opt-in. No NestJS é opt-out. Pequena mudança de paradigma, impacto real em segurança.

O que levaria para uma migração parecida

  • Instale as skills do Superpowers antes de começar. brainstorming para pensar a estratégia, writing-plans para o contrato de execução, subagent-driven-development para orquestrar, code-reviewer em cada wave. Sem essa estrutura, você gera código. Com ela, você executa engenharia.
  • Repositório paralelo, nunca branch de longa duração. Branch de migração que dura meses vira inferno de merge.
  • Mesmo banco, mesmo schema. Migração de dados em paralelo com migração de código multiplica o risco desnecessariamente.
  • Plano antes de qualquer código. Quanto mais preciso, menos interpretação livre dos agentes, menos re-trabalho.
  • Worktrees para paralelismo real. Módulos independentes implementados ao mesmo tempo, cada um em branch isolado.
  • Code review entre cada wave. Não só no final. O reviewer pegou race conditions, queries sequenciais desnecessárias e parâmetros sem validação antes de qualquer merge.
  • Pense bem na estratégia de corte antes de começar. Nginx gradual faz sentido quando você quer validar módulo a módulo em produção ao longo de semanas. Se a API vai ficar pronta antes disso, um corte único via frontend é mais simples e força um teste integrado de tudo ao mesmo tempo.
  • Valide com tráfego real antes de desligar o sistema antigo. Staging tem limite.

A migração está concluída. O NestJS está em produção servindo todas as 79+ rotas. O Fastify foi desligado.

GitHub
LinkedIn