02Critério5 min

Nunca começo do zero: o que sempre levo e o que sempre corto

Dez projetos sobre o mesmo boilerplate. O primeiro commit de cada um se parece mais com o último do anterior do que com um projeto novo, e o segundo commit quase sempre apaga algo.

Por Salva Castells

Imagem de capa: Park taeho, CC BY-SA 4.0.
Neste post

O primeiro commit de um projeto meu se chama "Initial commit" e já traz auth, banco de dados, pagamentos, i18n e um painel de admin. O segundo, muitas vezes, se chama "remove useless things". Esse par de commits resume melhor do que qualquer coisa como eu trabalho: não começo do zero, começo clonando meu template e cortando.

Dez projetos assim dão um critério bem afiado sobre o que é núcleo e o que é ruído. Não é opinião: está no histórico, e nos lugares onde a mesma remoção se repete.

O que se ganha começando assim

Um SaaS de gestão de incidentes para uma empresa de transporte saiu do zero para produção em três dias. Dia um: clonar e tirar o que sobrava. Dia dois: esquema de incidentes, upload de imagens, drawer de detalhe, cache de listagens. Dia três: landing. Os três dias foram integralmente de domínio. Ninguém tocou num login.

Um SaaS de geração de imagens começou igual. No dia do "Initial commit" já havia esquema de imagens geradas, helper de S3, branding e tema. No dia seguinte já estava o pipeline de geração. Auth, Postgres com Drizzle, Stripe e i18n vieram prontos.

O que se economiza não é escrever o login. É o mês de decisões pequenas que ele arrasta: onde a sessão mora, como se protege uma rota nova, o que acontece quando o cookie expira, como os idiomas são declarados.

O que sempre fica

  • Auth com uma única porta. Não uma função requireSession() que cada página tem que lembrar de chamar, e sim um layout.tsx na árvore privada. Uma página nova fica protegida por onde você a coloca, não por lembrar de copiar a checagem.
  • Postgres com um ORM tipado e migrações versionadas. Aqui há um detalhe que leva um tempo para descobrir: se os snapshots do drizzle dessincronizam, o drizzle-kit generate faz diff contra um banco vazio e reemite CREATE TABLE do esquema inteiro. Conserta-se reconstruindo o snapshot inicial até o generate voltar a dizer que não há mudanças.
  • i18n desde o minuto zero. Colocar depois obriga a mexer em cada string do projeto. Colocar antes custa uma tarde.
  • Um admin mínimo. Listagem de usuários e quatro métricas. Nada mais.

O que sempre é cortado

O Stripe é o caso claro. Num app de registro de ponto, as rotas checkout, portal e webhook foram apagadas inteiras no mesmo dia do commit inicial: 338 linhas fora. Num template de visualizador interno, idêntico. Dois projetos diferentes, um ano de diferença, a mesma remoção.

A conclusão não foi tirar o Stripe do template, e sim colocá-lo atrás de uma flag:

export const USE_STRIPE = process.env.USE_STRIPE === "true";

export async function POST(request: NextRequest) {
  if (!USE_STRIPE) {
    return NextResponse.json({ error: "Stripe is not enabled" }, { status: 501 });
  }

Um projeto sem pagamentos agora coloca USE_STRIPE=false e segue. Com uma nuance que levou tempo para descobrir: o cliente do Stripe era null as unknown as Stripe quando a flag estava desligada, então o primeiro uso descuidado dava um TypeError opaco em vez de dizer o que estava acontecendo. Agora é um Proxy que lança um erro explícito.

O outro corte foi RLS. As tabelas nasceram com crudPolicy({ role: authenticatedRole, read: authUid(table.userId) }) de drizzle-orm/neon. Todas foram removidas. O motivo não é que RLS seja ruim: é que o app já fazia todos os seus acessos por uma conexão de admin a partir do servidor, então as políticas nunca eram avaliadas e davam uma sensação de segurança que não existia. Duas camadas de autorização das quais só uma está viva é pior do que uma só, porque ninguém sabe qual manda.

E o admin superdimensionado. O template do time acumulou meses de painel de administração e gestão de usuários, e acabou num commit que apaga tudo. Herda custo para todos os filhos e quase nenhum usa.

O custo diferido

Isto é o que ninguém conta sobre trabalhar em cima de um template: um bug na mãe já nasce clonado em todos os filhos.

O mais caro que tive foi uma linha no root layout: alternates.canonical: BASE_URL. Qualquer página que não sobrescrevesse seu canonical se autocanonicalizava para a home e caía do índice. Ficou ali meses, em cada clone. A regra que tirei disso: falta de canonical é inócua, um canonical errado não é. Desde então o layout compartilhado não coloca canonical e cada rota coloca o seu.

Outros dois do mesmo tipo:

  • .gitignore só ignorava .env.local. O primeiro passo natural num clone é cp .env.example .env. O primeiro commit levava as chaves junto.
  • BASE_URL hardcoded para o domínio do template. Dali penduravam metadataBase, os canonicals, o hreflang, as URLs de OG, o sitemap e a validação de mesma origem do checkout do Stripe. Tudo silenciosamente errado em qualquer clone, e sem um único erro no console.

A detecção de idioma era acceptLanguage.includes(locale): ignora a ordem de preferência do cliente e acerta por coincidência de substring, porque "sl-SI" contém "sl". Multiplicado pelos projetos que herdaram aquela linha.

Como o template-mãe aprende

Os consertos vão ao contrário: do filho para a mãe. Quando um projeto descobre algo, a correção sobe para o template e o próximo já nasce com ela. Isso só funciona se a correção vier acompanhada de um teste que falhe ao reintroduzir a falha, porque senão o próximo refactor "simplifica" a regra de volta ao bug.

O último endurecimento grande do meu template foi um PR de 39 pontos: idempotência do webhook do Stripe com uma tabela processed_stripe_events, identidade sempre a partir da sessão e nunca por argumento nas server actions, userId no WHERE de cada leitura de feedback, validação de mesma origem nas URLs de retorno do checkout. Menos 11.421 linhas, mais 7.268. Quase tudo o que foi apagado eram tutoriais que descreviam uma estrutura que já não existia.

E o projeto seguinte começou com chore: inicializa desde la plantilla endurecida. Esse é o ponto inteiro.

Um template não é economia de digitação, é um lugar onde guardar o que você já aprendeu. O que decide se ele serve não é o que traz, é a disciplina de subir para ele cada conserto em vez de deixá-lo no filho onde apareceu.