Evite perda acidental de dados em atualizações com semântica PATCH
Evite perda acidental de dados em atualizações usando semântica PATCH, listas de campos permitidos e padrões claros para que campos ausentes nunca sejam apagados.

Por que atualizações no estilo substituição apagam dados acidentalmente
Uma atualização por substituição diz ao servidor: “trate este corpo de requisição como o registro inteiro.” O que você enviar vira a nova verdade, e qualquer coisa que você não inclua passa a ser considerada ausente. É assim que campos são apagados.
Isso geralmente acontece quando um endpoint se comporta como PUT (substituição completa) mesmo que todos o chamem de “atualização”. O cliente envia um payload parcial, o backend mapeia isso para o modelo e então salva. Se um campo estiver ausente, alguns caminhos de código o definem como um valor vazio (""), null ou um padrão. O resultado parece uma exclusão silenciosa.
Exemplo: um usuário edita o perfil e só muda o nome exibido. O formulário envia { "displayName": "Sam" }. Se seu servidor substituir todo o perfil, campos como phone, address ou marketingOptIn podem de repente virar null ou false, mesmo que o usuário não os tenha tocado.
Isso costuma aparecer quando clientes não (ou não podem) enviar o registro completo, como:
- Formulários web que só enviam inputs visíveis
- Apps mobile que mandam “apenas os campos alterados” para economizar banda
- Painéis de administração onde alguns campos estão escondidos em abas ou por permissões
É fácil não perceber nos testes. Um teste atualiza um campo e verifica que ele mudou, mas não confirma se todos os outros campos permaneceram iguais. Usuários descobrem o problema depois, quando um endereço desaparece, uma configuração de notificação é resetada ou uma integração para de funcionar.
O primeiro passo é identificar endpoints onde “atualizar” é realmente “substituir”.
PUT vs PATCH: o que muda e o que fica
PUT e PATCH respondem a perguntas diferentes.
PUT significa: “Aqui está o recurso completo. Substitua o que você tem por isto.” Se o servidor tratar PUT como substituição real, qualquer coisa que o cliente não inclua pode ser removida ou resetada.
PATCH significa: “Aqui estão mudanças específicas. Aplique-as sobre o que já existe.” Foi feito para edições parciais, onde o cliente envia apenas os campos que quer alterar.
Isso importa porque muitos clientes se comportam como formulários de edição. Um app mobile pode enviar apenas { "displayName": "Mina" }. Se seu endpoint espera um objeto completo (PUT) mas recebe um parcial, você pode apagar campos como bio, photoUrl ou timezone.
Uma regra simples que evita a maioria das surpresas é definir o que “ausente” significa:
- Campo ausente: mantenha o valor armazenado.
- Presente com valor: atualize-o.
- Presente com
null: apague-o, mas somente se a limpeza for explicitamente permitida.
Se você usar PUT, não trate campos ausentes como “apagá-los”. Trate campos ausentes como um erro para que os clientes precisem enviar uma representação completa.
Quando a substituição completa (PUT) ainda é útil
Substituição completa funciona quando o cliente realmente possui o documento inteiro e pode enviar todos os campos com confiabilidade toda vez. Por exemplo, uma ferramenta interna de admin que edita um pequeno registro de configurações, ou um processo de sincronização que sempre tem um snapshot completo.
Se você não consegue garantir isso (a maioria das APIs públicas não consegue), use PATCH para edições e documente as regras para que os clientes não tenham que adivinhar.
Escolha e documente suas regras de atualização
A maioria dos bugs de campos apagados vem de um desalinhamento de equipe: o servidor pensa “substituir”, enquanto o cliente envia “apenas campos alterados”. Antes de mudar código, decida o que cada endpoint significa.
Para cada endpoint de atualização, defina:
- Modo de atualização: replace ou patch
- Campos ausentes: ignorados ou rejeitados
nullexplícito: permitido para limpar ou rejeitado- Propriedade: quais campos o cliente pode editar vs campos controlados pelo servidor
- Validação: o que é checado em toda atualização
A distinção “ausente vs null” é onde as equipes se queimam. Se o cliente omite phone, normalmente você quer deixá-lo inalterado. Se o cliente enviar "phone": null, isso pode significar “limpar”, mas somente se você permitir isso.
Consistência entre web, mobile e ferramentas de admin importa. Clientes diferentes frequentemente enviam formatos de payload diferentes, e um cliente no estilo substituição pode apagar dados criados por outro.
Uma checagem rápida: escolha um campo (como timezone) e descreva o que acontece para (1) ausente, (2) null e (3) string vazia. Se a equipe não responder rápido, as regras não estão claras o suficiente.
Use listas de campos permitidos para controlar o que pode ser atualizado
Uma lista de campos permitidos faz com que o servidor aceite mudanças apenas em campos específicos nomeados. Todo o resto é bloqueado ou rejeitado.
Isso ajuda de duas maneiras:
- Evita gravações acidentais em campos que a UI nunca quis mudar.
- Impede que clientes atualizem campos sensíveis ou controlados pelo servidor.
Campos controlados pelo servidor quase nunca deveriam ser graváveis a partir de um endpoint normal de “atualizar perfil/configurações”, por exemplo:
- role ou permissions
- flags de status da conta
- totais de cobrança
createdAt/updatedAt- flags internas como
isAdminouriskScore
Rejeite campos desconhecidos em vez de armazená-los silenciosamente. Aceitação silenciosa esconde erros de digitação, clientes desatualizados e formatos inesperados de payload.
Listas aninhadas para objetos complexos
Se você aceita objetos aninhados como address ou settings, aplique a mesma regra dentro deles. Liste o top-level e então liste as chaves aninhadas. Isso permite que settings.theme seja editável enquanto bloqueia algo inseguro como settings.isAdmin.
Passo a passo: implementar atualizações parciais seguras
Uma atualização parcial segura é “aplique estas mudanças”, não “substitua o registro”. O padrão mais confiável é: carregar o que existe, aplicar apenas o que o cliente enviou (e que tem permissão para mudar), validar e então salvar.
Um fluxo de implementação prático
Uma sequência repetível fica assim:
- Busque o registro atual no banco (e verifique propriedade de usuário/tenant).
- Construa um objeto
changesa partir do corpo da requisição usando uma lista de campos permitidos. - Valide as mudanças (tipos, formatos, limites de tamanho, enums).
- Aplique apenas os campos que estão presentes na requisição. Não grave padrões para campos ausentes.
- Salve e retorne o registro atualizado para que o cliente possa re-sincronizar.
Isso evita o modo de falha comum onde o cliente envia dois campos e o servidor sobrescreve dez outros com valores vazios.
Log sem vazar dados
Quando algo dá errado, você quer visibilidade sem armazenar valores sensíveis. Faça logs de metadados como:
- id do registro (e id do usuário/tenant)
- quais nomes de campos mudaram
- falhas de validação
Logue “updated: displayName, avatarUrl,” não o nome exibido real.
Trate ausente, null e padrões sem surpresas
A maioria dos bugs de “meus dados sumiram” vem de uma confusão: o servidor não consegue distinguir entre “o usuário não tocou nisso” e “o usuário quer limpar isto”.
Trate o payload como instruções:
- Ausente significa “deixe como está”.
nullsignifica “limpar”, mas apenas para campos onde limpar faz sentido.
Também decida como lidar com valores vazios. Uma string vazia não é o mesmo que ausente, e um array vazio não é o mesmo que null. Se um usuário apagar todas as tags, "tags": [] deve definir tags para nenhuma. Se o cliente enviar "tags": null, decida se isso significa “remover tags” ou “input inválido”, e então mantenha a decisão.
Evite aplicar padrões de criação durante atualizações. Padrões pertencem aos fluxos de criação. Em atualizações, padrões frequentemente se tornam destrutivos.
Proteja contra atualizações perdidas e condições de corrida
Mesmo com semântica PATCH, duas edições ainda podem sobrescrever uma à outra. O risco é o tempo.
Exemplo: um usuário abre “Editar perfil” no laptop e no celular. O celular atualiza displayName e salva. O laptop, ainda com dados antigos, atualiza bio depois. Sem uma checagem de frescor, o segundo save pode reverter partes do primeiro.
Use controle otimista de concorrência para que o servidor possa rejeitar edições obsoletas:
- Campo de versão: armazene um inteiro como
profileVersion; só atualize se coincidir. - Checagem
updatedAt: cliente envia timestamp visto por último; servidor atualiza só se estiver inalterado. - ETag + If-Match: o cliente prova que está editando a versão mais recente.
Em caso de conflito, retorne um erro claro (frequentemente HTTP 409 ou 412) e peça para o cliente recarregar.
Erros comuns que apagam campos
A maior parte da perda de dados em atualizações não é um problema de banco de dados. É um problema de contrato de API: o servidor trata campos ausentes como “por favor remova”.
Causas comuns:
- Usar semântica PUT com um cliente que envia apenas campos alterados
- Salvar um objeto completo construído a partir de estado de cliente obsoleto
- Atualizar objetos aninhados como um todo em vez de patch nas crianças (substituir
addressapagaaddress.line2se o cliente só enviouaddress.city) - Preencher campos ausentes com padrões durante validação ou normalização
Uma mentalidade mais segura é simples:
- Ausente: deixe como está
- Null: limpe (somente quando permitido)
- Desconhecido: rejeite
Checagens rápidas antes de enviar para produção
Antes de liberar um endpoint de atualização, faça uma passagem focada em um risco: atualizar um campo acidentalmente muda outros?
Uma lista curta:
- Confirme que todo caminho de atualização segue as mesmas regras de ausente/null.
- Aplique listas de campos permitidos no servidor (não só na UI).
- Teste que campos ausentes não alteram dados armazenados (atualize só
displayName, verifique queemail,phone,addresspermanecem idênticos). - Teste que
nulllimpa somente os campos que você explicitamente permite. - Adicione ao menos uma checagem de concorrência para que duas edições não se sobrescrevam.
Um cenário rápido que pega muitos problemas: pegue um registro real de staging com muitos campos preenchidos, envie uma atualização com apenas um campo, depois refetch e diff. Qualquer mudança inesperada é sinal de alerta.
Exemplo: uma edição de perfil que apaga campos não enviados
Um bug comum parece inofensivo: um usuário atualiza a foto do perfil, clica em Salvar, e mais tarde nota que o número de telefone sumiu. Nada foi deletado intencionalmente. Foi sobrescrito.
Veja como acontece. A tela de perfil só permite mudar a foto, então o cliente envia apenas esse campo. O servidor trata a requisição como substituição total e grava um novo registro usando só o que foi submetido.
Antes: atualização no estilo replace (apaga campos)
Existing record in the database:
{
"id": "u_123",
"displayName": "Sam",
"phone": "+1-555-0100",
"photoUrl": "https://cdn.example/old.png"
}
Client sends:
{ "photoUrl": "https://cdn.example/new.png" }
Server does (conceptually):
profile = request.body
save(profile)
Result: phone disappears because it wasn’t included.
Depois: semântica PATCH + lista de campos permitidos (mantém campos)
Ao invés de substituir todo o registro, trate o payload como mudanças e aceite apenas os campos que o endpoint deve editar.
allowed = ["photoUrl"]
changes = pick(request.body, allowed)
profile = loadProfile(userId)
profile = merge(profile, changes)
save(profile)
Agora só photoUrl muda. Todo o resto permanece.
Próximos passos: audite suas atualizações e corrija endpoints arriscados
Encontre cada endpoint que pode alterar dados salvos (perfis, configurações, billing, “update status”). Para cada um, compare o que existe no armazenamento, o que o cliente envia e o que o servidor grava. Se o servidor pode gravar mais campos do que a requisição contém, você tem um risco.
Uma checklist prática de auditoria:
- Procure handlers que sobrescrevem registros inteiros a partir de corpos de requisição.
- Faça com que cada endpoint seja ou substituição verdadeira (e rejeite payloads parciais) ou patch verdadeiro (aplique apenas campos permitidos e presentes).
- Adicione uma lista de campos permitidos por endpoint e rejeite campos inesperados.
- Padronize regras de ausente vs null para que clientes se comportem consistentemente.
- Audite jobs em background e ferramentas de admin também, não apenas APIs públicas.
Se você herdou uma base de código gerada por IA, endpoints de atualização são um ponto comum de falha porque handlers gerados costumam adotar por padrão comportamento de “substituir”. FixMyMess (fixmymess.ai) foca em diagnosticar e reparar esses tipos de problemas em produção, incluindo apertar semânticas de atualização, adicionar listas de campos permitidos e reforçar validação para que dados reais de usuários não sejam apagados.
Perguntas Frequentes
Should I use PUT or PATCH for updates?
Use PATCH para edições parciais para que campos ausentes permaneçam inalterados. Reserve PUT apenas para substituições completas, onde o cliente pode enviar todo o recurso de forma confiável a cada vez.
Why do fields get wiped when I “update” only one field?
Porque uma atualização no estilo substituição trata o corpo da requisição como o novo registro completo. Qualquer campo que você não enviar pode ser sobrescrito com null, um valor vazio, ou um padrão — o que parece uma exclusão silenciosa.
What’s the safest way to treat missing fields vs null?
Escolha uma regra clara e aplique-a no servidor: ausente significa “manter como está”, enquanto null significa “limpar” apenas para campos onde limpar é permitido. Se você não consegue suportar limpeza com segurança, rejeite null para esse campo.
Can web forms cause accidental data loss even if the user didn’t touch those fields?
Sim, e é comum. Muitos formulários só enviam inputs visíveis, então campos ocultos ou em abas não aparecem no payload. Se seu backend substituir o registro, esses campos não enviados podem ser redefinidos.
How do I prevent clients from updating fields they shouldn’t touch?
Use uma lista de campos permitidos (field allowlist) por endpoint e aplique mudanças apenas para chaves que sejam permitidas e estejam presentes na requisição. Campos desconhecidos devem ser rejeitados para que erros de digitação e payloads inesperados não alterem dados silenciosamente.
What’s a safe server-side pattern for partial updates?
Carregue o registro existente, construa um objeto changes selecionando apenas as chaves permitidas da requisição, valide as mudanças, depois faça merge e salve. Evite construir um modelo completo somente a partir do corpo da requisição.
How do I handle nested objects like address without wiping subfields?
Não trate objetos aninhados como substituições totais, a menos que esse seja o contrato. Faça patch nas chaves aninhadas individualmente (por exemplo address.city) para que enviar um campo aninhado não apague irmãos como address.line2.
Why are defaults dangerous in update endpoints?
Padrões (defaults) pertencem ao fluxo de criação, não ao de atualização. Se você aplicar padrões em atualizações, campos ausentes podem ser “preenchidos” e sobrescrever dados reais armazenados.
How do I stop two edits from overwriting each other (race conditions)?
Use controle otimista de concorrência para que clientes desatualizados não sobrescrevam mudanças mais recentes. Um número de versão, uma checagem de updatedAt ou ETag/If-Match permite que o servidor rejeite edições antigas com uma resposta de conflito clara.
What’s the quickest test to catch “wiped field” bugs before shipping?
Pegue um registro real com muitos campos preenchidos, envie uma atualização mudando apenas um campo, depois recupere e faça um diff do registro completo. Se algo mais mudou, seu caminho de atualização está substituindo ou aplicando padrões indevidamente.