building-backends
Esta é a habilidade fundamental para trabalho de backend. Leia-a antes de escrever qualquer código de servidor — ela define os padrões e os pontos inegociáveis para que todos os serviços sejam consistentes, e então delega para as habilidades específicas os procedimentos detalhados. Se uma regra aqui conflitar com uma convenção local do repositório, o repositório vence; se não houver conflito, siga-a.
Primeiro, entenda antes de construir
- Leia o código existente antes de adicionar algo. Combine com o framework, modelo de erro, camadas, nomenclatura e estilo de teste do repositório. Um padrão "correto" que luta contra a base de código é o padrão errado. Introduzir uma segunda maneira de fazer algo que o repositório já faz é um defeito, não uma melhoria.
- Esclareça o contrato, não a implementação. Conheça as entradas, saídas, casos de erro, quem chama e as expectativas de consistência/latência antes de escolher um design. A maior parte do retrabalho em backend vem de construir o contrato errado rapidamente.
- Prefira o óbvio. Soluções comprovadas, legíveis e óbvias em vez de engenhosas. O backend roda sem supervisão às 3h da manhã; otimize para o engenheiro de plantão que precisa entendê-lo meio sonolento, não para elegância.
Arquitetura — as regras
- Camadas com dependências apontando para dentro.
transporte (http) → serviços (casos de uso) → domínio (entidades/regras), eserviços → repositórios (E/S) → domínio. O domínio não importa nada para fora nem faz E/S. Se uma seta de importação apontar na direção errada, o design está quebrado. - Handlers orquestram, serviços decidem, repositórios fazem E/S. Um handler analisa/valida a entrada, chama um serviço, mapeia o resultado para uma resposta — sem lógica de negócio. Um repositório é o único lugar que toca em um banco de dados, fila ou HTTP externo.
- Mantenha o domínio testável sem infraestrutura. Se não for possível testar unitariamente a lógica central sem um banco de dados ou rede, a lógica está na camada errada.
- Uma única raiz de composição. Construa a configuração, conecte as dependências e inicie o servidor em um único lugar. Todo o resto recebe suas dependências; nada sai para construir globais singleton.
- Projete sem estado. O estado de sessão/scratch vive em um armazenamento compartilhado (banco de dados/cache), não na memória do processo, para que o serviço escale horizontalmente e sobreviva a uma reinicialização. Se você está armazenando em cache em uma variável de nível de módulo, pare e pense.
→ Para montar o esqueleto com essas camadas já conectadas, use api-service-scaffold.
Design de API — as regras
- Contratos explícitos e versionados. Versione a API (URL ou cabeçalho) desde o primeiro dia; nunca faça uma mudança que quebre a compatibilidade em um contrato já publicado — adicione a nova forma ao lado e descontinue a antiga.
- Valide toda entrada no limite para um valor tipado. O núcleo nunca vê um corpo de requisição bruto. Rejeite entrada inválida com 422 e detalhamento por campo; nunca confie no cliente.
- Um modelo de erro, mapeado uma vez. O código de domínio levanta erros tipados (
NotFoundError,ConflictError,ValidationError,UnauthorizedError…); um mapeador de limite único os transforma em status +{ code, message, details? }. Nunca vaze stack traces ou mensagens internas.404para "não encontrado" e "não é seu" — distingui-los vaza existência. - Formas consistentes. Mesmo envelope para sucesso, mesmo envelope para erros, paginação consistente (cursor em vez de offset para conjuntos grandes), uso consistente de maiúsculas/minúsculas nos campos. Um chamador deve aprender suas convenções uma vez.
- Idempotência para operações inseguras.
PUT/DELETEsão idempotentes por definição; tornePOSTs que podem ser repetidos seguros com uma chave de idempotência. Redes repetem — um cliente cobrado duas vezes é um bug de design. - Códigos de status corretos. 2xx sucesso, 4xx culpa do chamador, 5xx sua culpa. Uma falha de validação é 422, não 200-com-um-corpo-de-erro; uma falha de autenticação é 401/403, não 400.
A camada de dados — as regras
- O banco de dados é a fonte da verdade; imponha invariantes lá. Chaves estrangeiras, restrições de unicidade,
NOT NULLe verificações pertencem ao esquema, não apenas ao código da aplicação — o código da aplicação tem bugs e condições de corrida; o banco de dados não mente. - Transações encapsulam invariantes e são tão curtas quanto possível. Tudo que deve ser tudo-ou-nada vai em uma transação; não faça trabalho lento (chamadas externas, computação pesada) enquanto segura uma. Transações longas seguram bloqueios e deixam todos os outros com fome.
- Espere concorrência. Duas requisições atingirão a mesma linha ao mesmo tempo. Use bloqueio otimista (uma coluna de versão) ou isolamento apropriado; nunca faça leitura-modificação-escrita em uma viagem de ida e volta sem proteção.
- Migrações são compatíveis com versões anteriores e reversíveis. Nunca mude o significado de uma coluna em uma única implantação; expanda → migre → contraia. Nunca adquira um bloqueio que bloqueie a tabela em uma tabela quente. Toda migração é enviada com um rollback testado.
- Cuidado com o N+1. Busque dados relacionados em um conjunto, não em um loop de consultas. Perfis a contagem de consultas em endpoints reais, não apenas a latência.
- Indexe as consultas que você realmente executa — e apenas essas. Um índice não utilizado é amplificação de escrita sem benefício.
→ Para os mecanismos de migração (bloqueios, expandir-contrair, backfills, rollbacks), use db-migration-guardian.
Segurança — a linha de base (inegociável)
- Autentique e depois autorize, em toda requisição, no lado do servidor. Nunca confie em uma afirmação do cliente sobre quem ele é ou o que pode fazer. Verifique a propriedade em cada acesso a recurso — "Eu tenho um token válido" ≠ "este registro é meu".
- Parametrize toda consulta. Nunca use SQL construído com strings. O mesmo para qualquer interface injetável (shell, LDAP, template).
- Segredos vêm do ambiente/gerenciador de segredos, nunca do código. Nada sensível no código fonte, logs ou mensagens de erro. (Veja a habilidade
secrets-management.) - Menor privilégio em toda parte — usuários de banco de dados, credenciais de serviço, funções na nuvem com escopo exato para o que o serviço precisa.
- Limite de taxa e defina timeouts em endpoints de entrada e chamadas de saída. Uma chamada ilimitada ou um endpoint sem limite é uma negação de serviço à espera de acontecer.
- Valide e codifique nos limites para prevenir injeção e SSRF; trate toda entrada externa, incluindo de outros serviços internos, como não confiável.
Concorrência, jobs e falha
- Assuma que toda chamada externa falha. Defina timeouts, adicione novas tentativas limitadas com backoff + jitter e um disjuntor para dependências instáveis. Uma nova tentativa sem timeout é como uma dependência lenta derruba todo o serviço.
- Torne as operações idempotentes para que uma nova tentativa (sua ou do chamador) seja segura.
- Trabalho em segundo plano vai para uma fila, não no caminho da requisição. A requisição enfileira; um trabalhador processa; falhas caem em uma fila de letras mortas para inspeção, não em um silencioso
catch {}. - Falhe alto, degrade com elegância. Exponha erros à telemetria; quando uma dependência não crítica estiver fora, degrade (sirva dados obsoletos, pule o enriquecimento) em vez de 500 toda a requisição.
Observabilidade faz parte de "pronto", não é um complemento
Um serviço que você não pode depurar em produção não está pronto. Logs estruturados, métricas RED/USE e rastreamento vinculados a um único ID de correlação vão sendo adicionados conforme você constrói, não após o primeiro incidente.
→ Use backend-observability para os detalhes de instrumentação e as diretrizes de cardinalidade/PII.
Postura de teste
- Teste o comportamento através da superfície pública (o endpoint / o caso de uso), não internos privados — para que os testes sobrevivam a refatorações.
- Uma pirâmide, não um sorvete: muitos testes unitários rápidos na lógica de domínio/serviço, um conjunto focado de testes de integração sobre o banco de dados real e a fiação, alguns testes ponta a ponta em caminhos críticos.
- Cada correção de bug recebe um teste de regressão que falha antes da correção e passa depois.
- Testes determinísticos. Sem dependência de relógio, rede ou ordenação; semeie dados explicitamente; isole testes uns dos outros.
Procedimento para qualquer tarefa de backend
- Leia o código ao redor; fixe o contrato (entradas/saídas/erros/chamadores/consistência).
- Coloque o trabalho na camada correta; mantenha o domínio livre de E/S.
- Valide a entrada no limite; use o modelo único de erro; retorne o status correto.
- Coloque invariantes no banco de dados + uma transação curta; lide com concorrência; planeje migrações como expandir-contrair.
- Aplique a linha de base de segurança (autorização + propriedade, consultas parametrizadas, segredos, timeouts/limites).
- Torne chamadas externas e jobs resilientes e idempotentes.
- Instrumente enquanto constrói; adicione testes na camada correta, incluindo um teste de regressão para qualquer bug.
- Confirme com a
Definição de prontoda habilidade específica relevante.
Definição de pronto
- Camadas respeitadas (domínio não faz E/S; dependências apontam para dentro); uma única raiz de composição.
- Toda entrada validada no limite; um modelo de erro mapeado uma vez; códigos de status corretos; contrato versionado.
- Invariantes impostos no banco de dados; transações curtas; concorrência tratada; migrações compatíveis com versões anteriores + reversíveis.
- Linha de base de segurança atendida: verificações de autorização + propriedade, consultas parametrizadas, segredos externos, timeouts/limites de taxa presentes.
- Chamadas externas/jobs idempotentes e resilientes; trabalho em segundo plano fora do caminho da requisição.
- Observabilidade conectada (logs/métricas/rastreamento em um ID de correlação); testes nas camadas corretas, verdes, com cobertura de regressão.