Validation

Escopo: JavaScript. Idiomas específicos deste ecossistema.

Validação não é uma única coisa: é um pipeline com três responsabilidades distintas, cada uma no seu lugar: limpar a entrada, conferir formato e aplicar regras de negócio. Misturar essas camadas cria acoplamento, dificulta testes e abre brechas de segurança. Em JS, Zod é o padrão de fato para validação de esquema com tipos inferidos.

[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)Biblioteca JS de validação com schemas componíveis e tipo inferido
trust boundary (limite de confiança)Ponto onde dados externos viram dados confiáveis após validação

Misturar essas camadas cria acoplamento, dificulta testes e abre brechas de segurança.

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);
}

Schema validation com Zod

Zod valida shape, tipos e constraints, nunca regras de negócio. Centraliza o contrato técnico e elimina 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

Schema valida se o dado tem o formato correto. Regras de negócio validam se faz sentido no domínio: dependem de I/O (Input/Output, Entrada/Saída) (banco, serviços externos) e não pertencem ao 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 shape, domínio valida regras após
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);
}

Output filtering

Retornar a entidade direta vaza campos internos: passwordHash, deletedAt, internalFlags. Projete explicitamente o que sai, nunca o objeto do banco diretamente.

❌ 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);
}

Desenvolvido por @thiagocajadev · Fork baseado no repositório pmndrs/docs · Poimandres.