Programação Por Equipe DevBras

Capa do artigo: Mensagem de erro igual na tela e na API: um catálogo de regras para os dois

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.

  1. Dê um código estável a cada erro, como obrigatorio, email_invalido e curto. O código não muda quando o texto é revisado.
  2. Escreva o texto uma vez, no catálogo, dizendo o que fazer e não só o que está errado.
  3. Declare as regras por campo no mesmo módulo.
  4. Use a mesma função de validação no formulário, antes do envio, e na rota da API.
  5. Devolva os erros num formato fixo, com campo, código e mensagem.
  6. 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?

Avalie este artigo

Referência: http://www.devmedia.com.br/jquery-validate-validando-formularios/26490