09 de out. de 2025·8 min de leitura

Incompatibilidades entre frontend e backend que fazem formulários não serem salvos

Incompatibilidades entre frontend e backend fazem formulários parecerem bem-sucedidos enquanto nada é salvo. Alinhe DTOs, erros de validação, códigos de status e formatos de paginação.

Incompatibilidades entre frontend e backend que fazem formulários não serem salvos

O que significa quando um formulário envia mas nada é salvo

Um contrato frontend-backend é o acordo compartilhado entre seu formulário e sua API: quais campos são enviados, como se chamam, que tipos têm e o que a API retorna em sucesso ou falha.

Quando um formulário “envia” mas nada é salvo, geralmente significa que o frontend fez a sua parte (enviou uma requisição), mas o backend não persistiu os dados. O problema difícil de perceber é que o usuário frequentemente não vê um erro claro porque a resposta parece OK, a UI ignora a resposta ou a API retorna um formato inesperado.

Um exemplo comum: o formulário posta { email, password }, mas a API espera { userEmail, passwordHash }. A requisição chega ao servidor, a validação falha, e a API retorna um 200 genérico com { ok: false } (ou um corpo vazio). O frontend trata qualquer 200 como sucesso, mostra uma notificação e o usuário segue adiante mesmo que o banco de dados nunca tenha mudado.

Isso acontece bastante em protótipos construídos rápido ou gerados por IA. Ferramentas conseguem gerar uma UI e uma API rapidamente, mas frequentemente chutam nomes de campos, esquecem regras de validação no servidor ou inventam formatos de resposta. Quando você mais tarde troca um endpoint mock por um real, o “contrato” muda ligeiramente e os salvamentos começam a falhar silenciosamente.

Corrigir isso não é só uma mudança em um arquivo. Normalmente você precisa alinhar algumas peças pela stack inteira:

  • Request DTOs (nomes dos campos, tipos, obrigatórios vs opcionais)
  • Erros de validação (um formato consistente que a UI possa mostrar próximo aos campos)
  • Códigos de status (para que sucesso e falha sejam inequívocos)
  • Respostas de sucesso (retornar o que a UI precisa para confirmar o salvamento)
  • Respostas de listagem (formato de paginação que não muda por endpoint)

Se você herdou um app gerado por IA e os salvamentos estão instáveis, esse é exatamente o tipo de problema que times como o FixMyMess veem: o código “roda”, mas o contrato é inconsistente. O restante deste guia foca em tornar esse contrato explícito, previsível e difícil de quebrar por acidente.

Sintomas comuns e o que eles normalmente indicam

Um formulário pode parecer ter funcionado enquanto nada mudou. A pista mais comum é uma notificação de sucesso (ou um check verde), mas após um refresh os dados antigos continuam lá. Isso geralmente indica que a UI decidiu que foi um sucesso com base no sinal errado, não no que o servidor realmente fez.

No backend, fique atento a respostas que parecem “bem-sucedidas” mas não são. Um problema clássico de contrato é um 200 OK que contém um erro dentro do JSON (por exemplo, { "ok": false, "message": "invalid" }). Outro é 204 No Content mesmo que o frontend espere o registro salvo de volta e precise de um id ou de campos atualizados.

Do lado do desenvolvedor, as pistas costumam ser pequenas e fáceis de ignorar: um console mostra undefined para um campo que você tem certeza que preencheu, ou a aba Network mostra um formato de resposta que você não codificou (como data vs result, ou um array onde você esperava um objeto). São incompatibilidades de contrato frontend-backend à vista.

Sintomas comuns e suas causas prováveis:

  • Mensagem de sucesso aparece, mas ao atualizar nada mudou: a requisição nunca atingiu o endpoint de salvamento, ou chegou com campos ausentes/renomeados.
  • Salvar funciona só para alguns usuários: a validação do backend difere das regras do frontend, ou campos obrigatórios dependem do papel do usuário.
  • Backend retorna 200, mas a UI se comporta estranhamente: erro está codificado dentro do JSON, não via código de status.
  • UI mostra “Salvo” mas a lista ainda mostra o item antigo: dados em cache não foram invalidados, ou a resposta não inclui o registro atualizado.
  • Paginação parece quebrada (itens faltando, repetidos): frontend espera page/total, backend retorna nextCursor/items (ou o contrário).

Regra rápida: confie mais na aba Network do que na UI. Se o payload da requisição e a resposta não corresponderem ao que seu código assume, o formulário pode “enviar” sem salvar de fato.

Isso é um padrão comum que vemos no FixMyMess quando um protótipo gerado por IA liga o botão e a notificação, mas nunca confirma que o servidor realmente persistiu algo.

O contrato que vocês devem concordar: formatos, nomes e tipos

Quando um salvamento “funciona” na UI mas nada muda no banco, muitas vezes não é um bug isolado. É um desacordo entre o cliente e a API sobre como um request e uma response válidos devem parecer. Muitas incompatibilidades são detalhes chatos, mas que quebram o app silenciosamente.

Comece escrevendo o contrato mínimo para um único salvamento. Se alguma parte for vaga, diferentes partes da stack preencherão as lacunas de formas diferentes.

  • Endpoint + método (por exemplo, POST /users vs PUT /users/:id)
  • Headers obrigatórios (especialmente Content-Type e auth)
  • Formato do body da requisição (nomes de campos, aninhamento, opcionais vs obrigatórios)
  • Formato da resposta (o que o cliente deve ler para atualizar a UI)
  • Formato de erro (como problemas de validação são retornados)

Nomeação é o primeiro lugar onde contratos derivam. Se o frontend envia firstName mas o backend espera first_name, você pode receber uma resposta “de sucesso” enquanto o backend ignora o campo desconhecido ou armazena um valor padrão.

Tipos são o segundo. Um caso comum: a UI envia age: "32" como string, mas o backend espera um número. Alguns frameworks fazem coerção, outros rejeitam, e outros convertem falhas em null. Se null for permitido, você acaba salvando um valor vazio sem perceber.

Campos extras ou faltantes também podem desaparecer silenciosamente. Por exemplo, o formulário inclui marketingOptIn, mas o DTO no servidor não o contempla. Dependendo da sua stack, esse campo pode ser descartado durante a desserialização sem erro. O inverso também é doloroso: o backend exige companyId, mas o frontend nunca envia, então o servidor cria um registro que não está associado a nada.

Uma maneira prática de detectar isso cedo é pegar uma requisição real nas ferramentas do navegador, comparar linha a linha com o DTO e as regras de validação do servidor, e concordar nos nomes e tipos exatos antes de mexer na lógica. Esse é o tipo de incompatibilidade que o FixMyMess normalmente encontra rapidamente durante uma auditoria de código em protótipos gerados por IA.

Passo a passo: alinhe DTOs dos campos do formulário até o banco

Quando um formulário “envia” mas nada salva, a causa usual é simples: o frontend está enviando um formato e o backend espera outro. Corrigir começa decidindo quem define a verdade e depois verificando cada etapa do formulário até o armazenamento.

1) Escolha uma única fonte de verdade

Escolha um lugar que defina nomes e tipos. Para a maioria dos times, os DTOs de request/response no backend são a fonte mais segura, pois ficam ao lado da validação e da persistência. Se você usa um schema compartilhado, trate-o como contrato e versionamento.

2) Escreva os DTOs com exemplos reais

Não confie em “é óbvio”. Escreva um exemplo para create e um para update. Updates frequentemente falham porque exigem um id, permitem campos parciais ou usam nomes diferentes.

// Create
{ "email": "[email protected]", "displayName": "Sam", "marketingOptIn": true }

// Update
{ "id": "usr_123", "displayName": "Sam Lee", "marketingOptIn": false }

Depois, escreva o response DTO que a UI realmente precisa. Se a UI espera user.id mas a API retorna userId, os salvamentos podem “funcionar” e ainda assim a UI não renderizar o estado atualizado.

3) Trace o caminho do formulário até o banco

Percorra a cadeia completa ao menos uma vez, ponta a ponta:

  • Nomes e tipos dos campos do formulário (strings, números, booleanos)
  • Payload enviado pela rede (incluindo headers como content type)
  • Parsing e validação do DTO no backend (campos obrigatórios, valores padrão)
  • Mapeamento para colunas do banco (nomes e conversões de tipo)
  • Corpo da resposta que a UI lê para atualizar a tela

4) Verifique usando o payload exato que a UI envia

Copie a requisição real da aba Network do navegador e reproduza-a. Isso captura problemas como "true" (string) vs true (boolean), campos faltantes ou aninhamento inesperado.

5) Mude um lado e depois reteste com um exemplo conhecido bom

Corrija ou o mapeamento do frontend ou o DTO do backend, não os dois ao mesmo tempo. Mantenha um payload “golden” e a resposta esperada para confirmar que você não apenas moveu a incompatibilidade para outro lugar.

Se você herdou código gerado por IA, essas incompatibilidades são comuns porque UIs e APIs geradas geralmente evoluem separadamente. Plataformas como o FixMyMess normalmente começam auditando o contrato e os pontos de mapeamento antes de mexer na lógica de negócio, porque é aí que as falhas silenciosas de salvamento se escondem.

Torne os erros de validação consistentes e fáceis de mostrar

Pare as falhas de salvamento silenciosas
Receba uma auditoria de código gratuita para descobrir por que seu formulário envia mas nada persiste.

Quando frontend e backend discordam sobre como os erros aparecem, os usuários têm a pior experiência: o formulário “envia”, mas nada diz o que ajustar. Um formato de erro simples e previsível é uma das vitórias mais fáceis contra incompatibilidades de contrato.

Um padrão prático é sempre retornar a mesma estrutura para falhas de validação:

{
  "error": {
    "type": "validation_error",
    "fields": [
      { "field": "email", "code": "invalid_format", "message": "Enter a valid email." },
      { "field": "password", "code": "too_short", "message": "Password must be at least 12 characters." }
    ],
    "non_field": [
      { "code": "state_conflict", "message": "This invite has already been used." }
    ]
  }
}

Mantenha três peças para cada problema: o nome do campo (casando com seu DTO), um código estável (para o UI reagir) e uma mensagem humana (para exibir). Se você retornar só mensagens, a UI acaba adivinhando e quebra quando o texto muda.

Erros não ligados a um campo importam tanto quanto. Permissões, conflitos de estado e limites de taxa não estão vinculados a um único input, então devem ir para um lugar separado como non_field (ou global). A UI pode mostrar esses erros perto do botão de envio ou como um pequeno banner.

No frontend, o mapeamento deve ser entediante e consistente:

  • Limpar erros anteriores antes do envio.
  • Para cada item em fields[], anexar message ao input cujo name bate com o campo.
  • Se o campo for desconhecido, tratar como erro global (frequentemente sinal de drift no DTO).
  • Mostrar non_field[] em um local visível.

Por fim, não esconda validação dentro de respostas “bem-sucedidas”. Se o salvamento falhou, retorne uma resposta de erro com um corpo de erro. Misturar avisos num 200 é como se obtivesse falhas silenciosas, especialmente em apps gerados por IA que vemos no FixMyMess.

Códigos de status e respostas de sucesso que não mentem

Muitas incompatibilidades de contrato começam com uma mentira simples: o servidor retorna um status de sucesso, mas a UI não sabe se o salvamento realmente ocorreu. Se o frontend trata qualquer 200 como “salvo”, você obtém o clássico “toast diz sucesso, mas ao atualizar nada há”.

Use códigos de status como sinal claro e mantenha o formato da resposta honesto.

Um padrão simples e previsível

Escolha regras que você possa seguir sempre:

  • 201 Created quando um novo registro for criado, e inclua o novo recurso no corpo.
  • 200 OK para leituras e atualizações, com um JSON que represente o estado salvo.
  • 204 No Content somente quando realmente não retornar corpo (e o cliente não precisar de novos dados).
  • 422 Unprocessable Entity para problemas de validação (erros de campo que o usuário pode corrigir).
  • 409 Conflict para duplicatas ou conflitos de versão (a requisição é válida, mas não pode ser aplicada como está).

Retornar 200 OK com um objeto { error: ... } é uma armadilha. Muitos frontends só checam response.ok ou o código HTTP. A UI mostrará sucesso enquanto o backend recusou silenciosamente o salvamento.

Idempotência, duplicações e comportamento de "tentar de novo"

Se usuários podem clicar duas vezes em salvar, atualizar no meio do processo ou tentar novamente depois de um timeout, você precisa de uma regra clara para duplicatas.

Use 409 quando o mesmo valor único já existir (por exemplo, email único) ou quando o locking otimista falhar (stale updatedAt ou version). Use 422 quando o payload em si estiver errado (campos obrigatórios faltando, formato inválido).

O que uma resposta de sucesso deve retornar

Mesmo em updates, retorne os dados canônicos que o servidor armazenou, não apenas um eco do que o cliente enviou. Uma boa resposta de salvamento costuma incluir:

  • id
  • updatedAt (ou version)
  • os campos normalizados (strings aparadas, defaults aplicados)
  • quaisquer valores gerados pelo servidor (slugs, status)

Exemplo: se o frontend envia " Acme ", a resposta deve retornar "Acme". Assim a UI já bate com a realidade e você pega problemas de contrato cedo. Times trazem APIs quebradas geradas por IA para o FixMyMess justamente quando uma resposta “bem-sucedida” na verdade escondia um salvamento recusado sob um 200.

Formatos de paginação que permanecem estáveis pela stack

Paginação é um contrato, não um detalhe de implementação. Se frontend e backend discordam sobre o formato, você terá tabelas vazias, linhas repetidas ou “Carregar mais” que nunca acaba. Essas incompatibilidades são comuns em APIs geradas por IA onde UI e servidor foram scaffolded separadamente.

Escolha um estilo de paginação e nomeie-o claramente

A maneira mais rápida de evitar confusão é escolher um estilo e escrever os parâmetros exatos:

  • page + pageSize: simples para números de página, mas pode ser lento se o banco precisar contar e pular muitos registros.
  • offset + limit: fácil de implementar, mas inserções e deleções podem causar duplicatas ou itens faltando.
  • cursor: melhor para scroll infinito, estável sob mudanças, mas precisa de um token cursor e uma ordenação estrita.

Depois de escolher, mantenha consistente entre endpoints. Uma UI feita para page=3&pageSize=20 não vai se comportar bem se um endpoint passar a esperar offset=40&limit=20 silenciosamente.

Congele o formato de resposta que a UI lê

Decida os campos exatos que o frontend pode confiar. Um padrão seguro é: items mais uma forma de saber se há mais dados. Totais são opcionais e podem ser caros.

Um erro comum é o backend retornar { data: [...] } enquanto a UI espera [...] (ou items). A requisição tem sucesso, a UI não renderiza nada e ninguém vê um erro.

Para evitar reorganizações de página, trave essas regras no contrato:

  • Sempre exigir uma ordenação determinística (por exemplo sort=createdAt:desc).
  • Aplicar filtros antes da paginação e, se possível, retornar os filtros aplicados.
  • Para paginação por cursor, basear o cursor nos mesmos campos de ordenação que você retorna.
  • Ser consistente sobre estados vazios: retornar items: [] com hasMore: false.

Quando o FixMyMess audita protótipos quebrados, paginação instável costuma ser a causa oculta de “nada foi salvo”, porque o registro salvo existe, mas nunca aparece na lista que o usuário verifica depois de salvar.

Armadilhas comuns que causam falhas silenciosas de salvamento

Reparar seu app gerado por IA
FixMyMess repara aplicativos Lovable, Bolt, v0, Cursor e Replit gerados por IA para produção.

Um formulário pode parecer saudável e ainda falhar em persistir porque UI e API discordam em pequenas diferenças fáceis de perder. Essas incompatibilidades frequentemente não geram um erro óbvio, então as pessoas assumem que o banco é que está falhando quando, na verdade, o formato da requisição está.

Uma armadilha comum é a coerção silenciosa de JSON. O frontend envia uma string onde a API espera um número, ou envia uma string vazia para um campo nullable. Alguns servidores silenciosamente descartam campos que não conseguem parsear, e então o salvamento falha depois porque o campo ausente é obrigatório.

Outro clássico: campos que parecem opcionais na UI mas são obrigatórios para salvar. Apps multi-tenant frequentemente precisam de tenantId, orgId ou userId. Se esses geralmente vêm do contexto de autenticação, um pequeno bug de auth pode deixá-los vazios sem alterar o formulário.

Datas também causam falhas sutis. Um date picker pode enviar "01/02/2026" enquanto a API espera ISO "2026-02-01". Timezones podem deslocar valores. Você salva "14 Jan" mas o servidor armazena "13 Jan" em UTC e parece que o salvamento não funcionou.

Desalinhamentos de contexto de autenticação são sorrateiros. A UI mostra você logado porque tem um token, mas a API trata a requisição como anônima porque o header está faltando, o cookie é bloqueado ou o token expirou.

Updates otimistas podem esconder tudo isso. A tela atualiza como se o salvamento tivesse sucedido, mas o backend rejeitou a requisição.

Fique de olho nesses sinais:

  • A aba Network mostra 200, mas o corpo da resposta diz "error" ou retorna um registro sem alterações.
  • A API retorna 204 e a UI não consegue confirmar o que foi salvo.
  • IDs obrigatórios estão faltando no payload, mas nenhum erro de campo é mostrado.
  • Uma data parece certa na UI mas difere no banco de dados.
  • A UI atualiza antes da conclusão da chamada API.

Ao auditar apps gerados por IA no FixMyMess, frequentemente encontramos duas formas de DTO usadas em telas diferentes, de modo que uma página salva e outra falha silenciosamente com os mesmos campos do formulário.

Checklist rápido antes de enviar para produção

Um formulário que “envia” não é o mesmo que dados realmente salvos. Antes do lançamento, faça uma checagem rápida de contrato que cubra todo o caminho: campos da UI, DTOs da API, validação e o que o servidor retorna.

Comece coletando exemplos reais, não só tipos. Coloque uma requisição de exemplo e uma resposta de exemplo ao lado do código e confirme que combinam com o que o servidor realmente recebe e retorna. Preste atenção em nomenclatura (camelCase vs snake_case), campos opcionais vs obrigatórios e peculiaridades de tipo como números chegando como strings.

Aqui está uma checklist curta que pega a maioria das falhas silenciosas:

  • Confirme que os DTOs de create e update batem com os campos da UI exatamente (nomes, tipos e quais campos podem ser null ou ausentes).
  • Faça com que todo salvamento falho retorne um código não 2xx, mais um corpo de erro consistente (incluindo uma mensagem geral e erros por campo).
  • Assegure que a UI consiga mapear erros de campo do servidor para os nomes exatos de inputs que o usuário vê (sem "emailAddress" no servidor se o formulário usa "email").
  • Verifique que todos os endpoints de listagem usem o mesmo formato de paginação de resposta (chave items, contagem total, page/limit e onde metadata vive).
  • Teste um create real e um update real end-to-end com um registro de banco real, e então atualize a página para confirmar que os valores persistem.

Um teste prático rápido: submeta intencionalmente um campo inválido (como uma senha muito curta). Se a UI mostra uma notificação de sucesso, ou a rede mostra 200 enquanto nada mudou, seu contrato está mentindo em algum lugar.

Se você herdou um app gerado por IA, é aí que os problemas se acumulam: DTOs divergem, formatos de erro variam por endpoint e paginação é reinventada por tela. Times como o FixMyMess normalmente começam com uma auditoria rápida focada nesses contratos para que os salvamentos fiquem previsíveis antes de adicionar mais features.

Um exemplo realista: o salvamento parece OK, mas os dados ficam errados

Preparar para deploy em produção
Prepare sua aplicação para deployment com reparos, endurecimento e verificações.

Um caso comum: um formulário de cadastro mostra uma notificação de sucesso, mas o usuário não consegue logar depois. Todo mundo assume “auth quebrou”, mas o bug real é o formato da requisição e da resposta.

O frontend envia este payload:

{
  "email": "[email protected]",
  "password": "P@ssw0rd!",
  "passwordConfirm": "P@ssw0rd!"
}

A API espera password_confirmation (snake_case) e ignora passwordConfirm. Se a API também retorna 200 OK com um genérico { "success": true }, a UI vai comemorar mesmo que o servidor nunca tenha validado a confirmação e possa ter armazenado um valor incorreto ou rejeitado internamente.

A correção é chata, mas funciona: concordem em um DTO e em um formato de erro. Ou renomeie o campo na UI, ou aceite ambas as chaves no servidor e mapeie para o mesmo DTO.

No sucesso, retorne algo que prove que o salvamento ocorreu:

{
  "id": "usr_123",
  "email": "[email protected]"
}

Use 201 Created para um novo usuário. Em falha de validação, use 422 Unprocessable Entity e retorne erros por campo que a UI possa mostrar ao lado dos inputs:

{
  "errors": {
    "password_confirmation": ["Does not match password"]
  }
}

Um segundo mini-caso aparece em páginas de listagem. O frontend cria controles de paginação baseado em total, mas a API só retorna um cursor e items. A UI renderiza “Página 1 de 0” ou desabilita Próxima mesmo quando há mais dados.

Escolha um estilo de paginação e mantenha. Se quiser totais, retorne items e total. Se preferir paginação por cursor, retorne items, nextCursor e hasNext, e faça a UI parar de pedir total.

Próximos passos: trave o contrato e evite regressões

Incompatibilidades de contrato continuam acontecendo por um motivo: o contrato vive na cabeça das pessoas, não em algo verificável. A correção é chata, mas eficaz: escreva, teste e trate mudanças como mudanças reais.

Comece com uma nota de página única para os endpoints que mais importam (normalmente: create, update, list). Mantenha em linguagem simples e inclua exemplos concretos.

  • Request DTO: nomes de campos, obrigatórios vs opcionais, tipos e como valores vazios são enviados
  • Response DTO: o que “sucesso” retorna (registro salvo vs só um id)
  • Formato de erro: uma única forma para erros de validação e de servidor, com alguns exemplos
  • Códigos de status: o que usar para create/update/not found/validação
  • Paginação: parâmetros e formato de resposta (items, total, page, pageSize)

Depois, adicione um pequeno conjunto de checagens de contrato para endpoints chave para pegar quebras no mesmo dia em que são introduzidas. Pode ser testes snapshot no backend, ou um script simples em CI que posta payloads conhecidos e asserta sobre formato e status da resposta.

Escolha uma lista curta de regras “nunca mudar silenciosamente” e aplique-as:

  • Erros de validação sempre mapeiam para campos (e incluem uma mensagem legível)
  • Sucesso nunca retorna 200 com uma mensagem de erro escondida no corpo
  • Paginação sempre retorna as mesmas chaves, mesmo com items vazio
  • DTOs não renomeiam campos sem bump de versão ou release coordenado

Antes de polir a UI, padronize o formato de erro da API. Assim que o frontend puder exibir erros de campo de forma confiável, a maior parte dos relatos “salvou mas não salvou” fica muito mais clara.

Se sua base de código foi gerada por ferramentas de IA e os padrões estão inconsistentes, uma auditoria focada e um passe de reparo podem ser o caminho mais rápido para estabilidade. O FixMyMess faz auditorias gratuitas de código e depois repara contratos de ponta a ponta (DTOs, validação, códigos de status, paginação) para que o app se comporte em produção como nas demos.