Mensagem de erro igual na tela e na API: um catálogo de regras para os dois
Centralize códigos e textos de erro de validação num módulo usado pelo formulário e pelo servidor, devolva os erros num formato previsível e teste que os dois lados dizem o mesmo.
A pessoa digita o e-mail, a tela diz "Informe um e-mail válido", ela corrige, envia, e a API responde "formato inválido". Ela não sabe se errou de novo ou se é outra regra, e corrige o campo pela terceira vez. A resposta curta: código e texto de cada erro de validação vivem num lugar só. O formulário e o servidor usam o mesmo catálogo de regras, a API devolve o código e a mensagem por campo, e um teste automatizado falha no dia em que a frase da tela e a da API deixarem de ser iguais.
O campo é um só. A frase também. Quando divergem, quase nunca é detalhe de tom: são duas regras diferentes, e uma delas está errada.
Por que as mensagens divergem
A divergência nasce da divisão de trabalho. Quem faz a tela instala um plugin de validação, como o jQuery Validation, e aceita as mensagens padrão dele, que vêm em inglês se ninguém carregar a tradução. Ou usa a validação nativa do HTML, cujas mensagens dependem do navegador e do idioma dele. Quem faz a API escreve as próprias frases no servidor. Cada lado tem a sua cópia da regra e do texto.
Com o tempo, as regras também se separam. O servidor passa a exigir senha de 10 caracteres, a tela continua aceitando 8, e a pessoa só descobre depois de enviar. O post validação de plugin não dispensa a regra no servidor explica por que o servidor precisa validar de qualquer jeito. Aqui o ponto é outro: já que os dois validam, que digam a mesma coisa.
Como montar um catálogo único de regras
O exemplo é o cadastro de uma loja, com nome, e-mail e senha. O front-end e o back-end são JavaScript, então o mesmo arquivo roda nos dois. Com servidor em outra linguagem, o catálogo vira um JSON lido pelos dois lados.
- Dê um código estável a cada erro, como
obrigatorio,email_invalidoecurto. O código não muda quando o texto é revisado. - Escreva o texto uma vez, no catálogo, dizendo o que fazer e não só o que está errado.
- Declare as regras por campo no mesmo módulo.
- Use a mesma função de validação no formulário, antes do envio, e na rota da API.
- Devolva os erros num formato fixo, com campo, código e mensagem.
- Teste que os dois caminhos devolvem a mesma lista para a mesma entrada inválida.
O módulo compartilhado
// regras.js (usado pelo navegador e pelo Node)
export const MENSAGENS = {
obrigatorio: (c) => 'Informe ' + c.rotulo + '.',
email_invalido: () => 'Informe um e-mail no formato nome@dominio.com.br.',
curto: (c) => 'O campo ' + c.nome + ' precisa ter pelo menos ' + c.minimo + ' caracteres.'
};
export const CAMPOS = {
nome: { nome: 'nome', rotulo: 'o seu nome', obrigatorio: true },
email: { nome: 'e-mail', rotulo: 'o seu e-mail', obrigatorio: true, email: true },
senha: { nome: 'senha', rotulo: 'uma senha', obrigatorio: true, minimo: 10 }
};
function pareceEmail(valor) {
const partes = valor.split('@');
return partes.length === 2 && partes[0] !== '' && partes[1].includes('.') && !valor.includes(' ');
}
export function validar(dados) {
const erros = [];
for (const [campo, regra] of Object.entries(CAMPOS)) {
const valor = String(dados[campo] ?? '').trim();
let codigo = null;
if (regra.obrigatorio && valor === '') codigo = 'obrigatorio';
else if (regra.email && !pareceEmail(valor)) codigo = 'email_invalido';
else if (regra.minimo && valor.length < regra.minimo) codigo = 'curto';
if (codigo) erros.push({ campo, codigo, mensagem: MENSAGENS[codigo](regra) });
}
return erros;
}
A conferência de e-mail é deliberadamente simples. A única prova de que um e-mail existe é uma mensagem que chega nele; a validação só pega erros de digitação óbvios.
Como escrever o texto de cada erro
Já que o texto vai aparecer em dois lugares, vale caprichar nele uma vez. Diga o que a pessoa precisa fazer, não só o que deu errado: "A senha precisa ter pelo menos 10 caracteres" resolve, "Senha inválida" não. Inclua o limite quando houver um número envolvido, e use o nome do campo como ele aparece no rótulo da tela.
Cuidado também com a validação nativa do HTML. Atributos como required e type="email" geram balões com mensagens do navegador, em frases que mudam de um navegador para outro. Se você quer o catálogo como única fonte, desligue esses balões com novalidate no formulário e mantenha os atributos pelo teclado certo no celular e pela semântica.
A rota da API
// servidor.js
import express from 'express';
import { validar } from './regras.js';
export const app = express();
app.use(express.json());
app.post('/api/cadastro', (req, res) => {
const erros = validar(req.body ?? {});
if (erros.length > 0) {
return res.status(422).type('application/problem+json').json({
type: 'https://loja.exemplo/erros/validacao',
title: 'Alguns campos precisam de correção.',
status: 422,
errors: erros
});
}
// grava o cadastro...
res.status(201).json({ ok: true });
});
O corpo segue o formato de Problem Details da RFC 9457, com um membro extra errors. Não é obrigatório usar esse padrão, mas ter um formato fixo é: a tela precisa saber onde procurar o campo e a mensagem sem tratar cada rota de um jeito. Os códigos de status em si estão no guia de códigos de status HTTP.
O formulário
No navegador, a mesma função roda antes do envio, e os erros que vierem do servidor são mostrados no mesmo lugar, com a mesma função de exibição. Se a tela usa o jQuery Validation, alimente a opção messages do plugin a partir do catálogo, em vez de deixar as mensagens padrão.
import { validar } from './regras.js';
function mostrarErros(form, erros) {
form.querySelectorAll('[data-erro-de]').forEach((el) => { el.textContent = ''; });
for (const erro of erros) {
const alvo = form.querySelector('[data-erro-de="' + erro.campo + '"]');
if (alvo) alvo.textContent = erro.mensagem;
}
}
const form = document.getElementById('cadastro');
form.addEventListener('submit', async (evento) => {
evento.preventDefault();
const dados = Object.fromEntries(new FormData(form));
const locais = validar(dados);
if (locais.length > 0) return mostrarErros(form, locais);
const resposta = await fetch('/api/cadastro', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(dados)
});
if (resposta.status === 422) {
const problema = await resposta.json();
mostrarErros(form, problema.errors);
}
});
Onde a mensagem aparece e como ela se liga ao campo para leitores de tela é assunto do post formulário que não cansa.
O teste que trava a divergência
O teste sobe a API numa porta livre, manda a mesma entrada inválida e compara com o resultado da função local. Se alguém mudar uma regra só no servidor, ele falha.
// cadastro.test.js
import test from 'node:test';
import assert from 'node:assert';
import { app } from './servidor.js';
import { validar } from './regras.js';
test('tela e API devolvem os mesmos erros', async () => {
const servidor = app.listen(0);
const { port } = servidor.address();
const dados = { nome: '', email: 'ana@loja', senha: '123' };
try {
const resposta = await fetch('http://localhost:' + port + '/api/cadastro', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(dados)
});
const corpo = await resposta.json();
assert.strictEqual(resposta.status, 422);
assert.deepStrictEqual(corpo.errors, validar(dados));
} finally {
servidor.close();
}
});
Erros comuns
- Mensagens padrão do plugin na tela: a pessoa lê inglês no formulário e português na resposta da API, e não sabe se são a mesma regra.
- Regra copiada nos dois lados: o limite muda num lugar só e a tela passa a aceitar o que o servidor recusa.
- Código de erro que é o próprio texto: qualquer revisão de redação quebra o front-end que comparava strings.
- Erro do servidor mostrado num alerta genérico: a pessoa não sabe qual campo corrigir e reenvia igual.
- Mensagem que só diz "inválido": a pessoa sabe que errou, mas não sabe o que o sistema espera.
- Formato de erro diferente por rota: cada tela ganha um tratamento próprio e algumas acabam sem nenhum.
Checklist
- Cada erro de validação tem um código estável e um texto único.
- Regras e mensagens estão num módulo ou arquivo lido pelos dois lados.
- Nenhuma mensagem padrão de plugin ou de navegador chega à pessoa sem passar pelo catálogo.
- A API devolve campo, código e mensagem num formato fixo.
- A tela mostra erros do servidor junto ao campo, no mesmo lugar dos erros locais.
- Um teste compara a saída da validação local com a resposta da API.
Pegue um e-mail sem domínio completo, envie pelo formulário e depois direto para a API com curl. As duas frases são idênticas? Você tem um teste que falha quando elas deixarem de ser?
Referência: http://www.devmedia.com.br/jquery-validate-validando-formularios/26490