Uma API interna (tRPC) em que o time confia
Toda equipe pequena tem um momento em que a API interna vira território de medo. Alguém mexe num endpoint, três telas quebram, ninguém entende por quê, e a partir daí cada mudança vem com um pedido de desculpas antecipado. A gente decidiu que não ia viver assim. A API interna do ClubPetro precisava ser um lugar onde você mexe com confiança — não porque escreveu 300 testes, mas porque o próprio desenho não deixa você errar em silêncio. É sobre isso que este post fala: como usamos tRPC, Zod e uma disciplina chata de camadas pra transformar a API interna em algo em que o time confia.
O contrato existe, e o compilador te obriga a respeitá-lo
O ponto de partida do tRPC não é "menos boilerplate". É que o tipo do backend viaja até o frontend sem você escrever nada. Quando você renomeia um campo de retorno num procedure, a tela que consome aquele campo para de compilar na hora. Não em runtime, não no cliente reclamando de undefined — no seu editor, enquanto você digita.
Isso muda o comportamento das pessoas. O medo de mexer some porque o custo de errar deixou de ser "descubro em produção" e virou "descubro antes de commitar". A gente trata isso como regra:
- Toda fronteira de entrada tem Zod. Input de procedure, webhook, variável de ambiente. Se um dado cruza a borda do sistema, ele passa por um schema. Não é cerimônia — é o único jeito de o "confia em mim" virar "o schema garante".
anyé proibido. Umanynum retorno de procedure é um buraco no contrato: ele apaga justamente a garantia que faz o time confiar. Quando precisamos de flexibilidade, éunknown+ type guard.- Zod erra com detalhe. Quando um input não bate, o erro já vem dizendo qual campo e por quê. Debugar uma chamada quebrada vira ler uma frase, não caçar no escuro.
O contrato tipado é a base da confiança. Mas contrato sozinho não impede que a API vire uma bagunça de responsabilidades. Pra isso, precisamos de camadas.
Procedures orquestram, domínio calcula
A regra mais importante da nossa API não está no tRPC — está em como a gente separa o que é orquestração do que é lógica de negócio. Um procedure tRPC não calcula health score, não decide se um contrato está inadimplente, não aplica regra de comissão. Ele busca dados, chama uma função pura em packages/domain, e devolve o resultado.
Por que isso importa pra confiança? Porque quando a lógica vive numa função pura, sem I/O, ela é previsível. Você lê a função e entende o que ela faz sem precisar entender de onde vieram os dados. E quando algo dá errado, você sabe em qual camada olhar: se o número está errado, é domínio; se o dado está errado, é o procedure ou o banco.
protectedProcedure,adminProcedure,csmProcedure,billingProcedure... A autorização é parte do contrato, não umifesquecido no meio da função. O tipo de procedure já diz quem pode chamar. Escolher o procedure errado (umprotectedProcedureonde devia seradminProcedure) é um bug de segurança — e a gente já achou alguns fazendo auditoria justamente porque o padrão torna esse tipo de erro visível.- Clients de banco sempre via singleton lazy.
getDb(),getSupabase(),getSupabaseAdmin(). Ninguém instancia conexão dentro de um procedure. Isso evita vazamento de pool e mantém o comportamento de conexão em um lugar só.
O que quebra, e como a gente aprendeu
Nem tudo é elegante. Duas armadilhas nos morderam de verdade e viraram conhecimento coletivo.
A primeira: o batch do tRPC devolve HTTP 200 mesmo quando um procedure individual falha. Se você monitora saúde da API pelo status HTTP, um erro real passa despercebido no meio de um batch bem-sucedido. Tivemos que aprender a olhar o corpo da resposta, não o envelope.
A segunda: datas em SQL cru. No Drizzle, quando você monta uma query com template sql, uma Date precisa ser passada como ISO string, não como objeto Date cru — o driver postgres-js estoura no bind. É o tipo de coisa que não aparece em nenhum tipo, só explode em runtime. Documentamos, e virou item de review.
- Confiança não é ausência de bugs. É saber onde o bug vai aparecer e ter o hábito de conferir esse lugar.
- O padrão que revela erros vale mais que o padrão que os esconde. Preferimos um desenho onde o erro grita cedo a um que é "limpo" mas silencioso.
O que faríamos de novo
Tudo. O tRPC com Zod na borda e domínio puro no miolo é o alicerce que mais nos deu tranquilidade pra evoluir rápido sem test suite formal. A camada de tipos faz o trabalho que testes fariam em outro time, e a separação de responsabilidades faz o debugging ser uma questão de saber onde olhar, não de adivinhar.
Se tem uma coisa que a gente reforçaria mais cedo, é a disciplina de nunca deixar um any passar "só por enquanto" e nunca colocar lógica de negócio dentro de um procedure "só porque é rapidinho". Esses dois atalhos são exatamente os que corroem a confiança que a arquitetura tenta construir.
Se você é o tipo de desenvolvedor que sente prazer em desenhar sistemas onde o erro aparece cedo e alto — onde o compilador é seu aliado e não seu inimigo — você ia gostar de mexer nessa API. Aqui a confiança não é uma qualidade abstrata que a gente pede no onboarding. É uma propriedade que a gente construiu no código, uma camada de cada vez, e defende em todo review.