Validation

Escopo: JavaScript. Idiomas específicos deste ecossistema.

Validação não é um passo único, e sim um pipeline (sequência de etapas) com três responsabilidades separadas: limpar a entrada, conferir o formato e aplicar as regras de negócio. Cada uma tem o seu lugar. Juntar as três em um só ponto acopla o código, dificulta o teste e abre brechas de segurança. Em JavaScript, Zod (biblioteca de validação) é a escolha padrão para validar o esquema e ainda derivar os tipos a partir dele.

[Input]SanitizeSchema ValidateBusiness Rules[Output Filter]Response

Conceitos fundamentais

ConceitoO que é
sanitization (saneamento)Limpeza de entrada: trim, toLowerCase, normalização de unicode; antes de validar
schema validation (validação de esquema)Conferência de formato: tipos, comprimento, presença de campos obrigatórios
business rule (regra de negócio)Validação que depende do estado do sistema (ex: email já cadastrado, saldo suficiente)
output filter (filtro de saída)Remoção de campos sensíveis ou internos antes de responder ao cliente
DTO (Data Transfer Object · Objeto de Transferência de Dados)Estrutura sem comportamento usada para mover dados entre camadas
parse, don't validate (transforme, não só verifique)Princípio: converter a entrada em tipo seguro de uma vez, em vez de só checar e seguir com unknown
Zod (biblioteca de validação)Você descreve o formato esperado; ela valida os dados e deriva o tipo a partir dessa descrição
trust boundary (limite de confiança)Ponto onde dados externos viram dados confiáveis após validação

Sanitização de entrada

Antes de validar, limpar: trim em strings, toLowerCase em emails. Dados sujos entram em validação suja: um email com espaço passa no schema mas falha na busca no banco.

❌ Ruim: dados brutos chegam direto na validação
async function createUserHandler(req, res) {
  const input = createUserSchema.parse(req.body); // " Admin@Email.com " passa no schema

  await createUser(input);
  res.status(201).json({ id: input.id });
}
✅ Bom: sanitize antes de validar
function sanitizeCreateUser(body) {
  const sanitized = {
    name: body.name?.trim(),
    email: body.email?.trim().toLowerCase(),
  };

  return sanitized;
}

async function createUserHandler(request, response) {
  const sanitized = sanitizeCreateUser(request.body);
  const input = createUserSchema.parse(sanitized);

  await createUser(input);

  const body = { id: input.id };
  response.status(201).json(body);
}

Validação de esquema com Zod

Zod confere formato, tipos e restrições, nunca regras de negócio. Reúne o contrato técnico em um lugar e apaga a validação manual espalhada pelos handlers.

❌ Ruim: validação manual espalhada no handler
async function createOrder(body) {
  if (!body.productId) throw new ValidationError("productId required");
  if (typeof body.quantity !== "number")
    throw new ValidationError("quantity must be number");

  if (body.quantity <= 0)
    throw new ValidationError("quantity must be positive");
  if (!body.customerId) throw new ValidationError("customerId required");
}
✅ Bom: schema centralizado, handler recebe dado tipado e validado
const createOrderSchema = z.object({
  productId: z.string().uuid(),
  quantity: z.number().int().positive(),
  customerId: z.string().uuid(),
});

async function createOrder(body) {
  const input = createOrderSchema.parse(body);
  const invoice = await buildInvoice(input);
  return invoice;
}

Regras de negócio

O schema confere se o dado tem o formato certo. As regras de negócio conferem se ele faz sentido no domínio: dependem de I/O (Input/Output · Entrada/Saída), como banco e serviços externos, e por isso não cabem no schema.

❌ Ruim: I/O dentro do schema (refine async) mistura camadas
const createOrderSchema = z.object({
  productId: z
    .string()
    .uuid()
    .refine(
      async (id) => {
        const product = await db.products.findById(id);
        return product?.isAvailable; // regra de domínio escondida no schema
      },
      { message: "Product not available" },
    ),
});
✅ Bom: schema valida formato, domínio valida regras depois
const createOrderSchema = z.object({
  productId: z.string().uuid(),
  quantity: z.number().int().positive(),
});

async function validateOrderRules(input) {
  const product = await findProductById(input.productId);
  if (!product) return Result.fail("Product not found", "NOT_FOUND");

  if (!product.isAvailable)
    return Result.fail("Product unavailable", "UNAVAILABLE");

  if (product.stock < input.quantity)
    return Result.fail("Insufficient stock", "OUT_OF_STOCK");

  return Result.ok(product);
}

async function createOrder(body) {
  const input = createOrderSchema.parse(body);

  const rulesResult = await validateOrderRules(input);
  if (!rulesResult.ok) return rulesResult;

  const invoice = await buildInvoice(input, rulesResult.value);

  return Result.ok(invoice);
}

Filtro de saída

Devolver a entidade direto do banco vaza campos internos: passwordHash, deletedAt, internalFlags. Monte de forma explícita o objeto que sai na resposta, campo a campo, em vez de entregar a linha do banco como está.

❌ Ruim: entidade direta vaza campos internos
async function findUserByIdHandler(req, res) {
  const user = await db.users.findById(req.params.id);

  return res.json(user); // passwordHash, internalFlags, deletedAt...
}
✅ Bom: projeção explícita do que sai na resposta
function toUserResponse(user) {
  const userResponse = {
    id: user.id,
    name: user.name,
    email: user.email,
    createdAt: user.createdAt,
  };

  return userResponse;
}

async function findUserByIdHandler(request, response) {
  const user = await userRepository.findById(request.params.id);
  const userResponse = toUserResponse(user);

  return response.json(userResponse);
}

DoDocs v3.7.0 · Desenvolvido por @thiagocajadev · Baseado no trabalho de pmndrs/docs · Poimandres.