MongoDB: Validação de dados e middleware do Mongoose

Última atualização: 2026-08-26

A validação de dados é a primeira linha de defesa na camada de aplicação — os validadores do Mongoose podem bloquear 80% dos dados inválidos.

Neste curso, você aprenderá sobre os validadores embutidos do Mongoose, os validadores personalizados, validateBeforeSave e o middleware por meio de exercícios práticos.

O papel dos validadores na estrutura de proteção de dados: A proteção abrangente de dados consiste em uma estrutura de três camadas — 1. Camada do banco de dados (validação de esquema do MongoDB/índices únicos): A última linha de defesa, impedindo que dados incorretos sejam inseridos no banco de dados devido a vulnerabilidades na camada de aplicação; 2. Camada de aplicação (validadores do Mongoose + middleware): A principal linha de defesa, interceptando a maioria dos dados inválidos e fornecendo mensagens de erro fáceis de entender; 3. Camada de front-end (validação de formulários): A camada de experiência do usuário, que fornece feedback imediato, mas não é segura (pode ser contornada). A relação entre essas três camadas: Validação front-end ≠ segurança (pode ser contornada), validação do Mongoose = base de segurança (não pode ser contornada), validação do banco de dados = rede de segurança (garante que nenhum dado inválido seja armazenado, mesmo que a camada de aplicação falhe). Nunca confie exclusivamente na validação front-end.

Quando os validadores do Mongoose são executados: Os validadores do Mongoose são executados automaticamente quando document.save() e document.validate() são chamados (com validateBeforeSave definido como true por padrão). Observação: 1. Operações de atualização, como Model.updateOne() e updateMany(), não acionam os validadores — elas operam diretamente no banco de dados, contornando a camada de documentos do Mongoose; 2. Para validar durante uma atualização, use Model.findOneAndUpdate com a opção runValidators: true; 3. Os validadores são executados antes do middleware pre('save') — se a validação falhar, o middleware pre('save') não será executado (o hash da senha não será realizado em dados inválidos). Compreender essa ordem de execução é fundamental para depurar problemas de validação.

1. O que você vai aprender


2. Validadores integrados

Explicação do conceito: Os validadores embutidos do Mongoose são regras de validação fornecidas pelo SchemaType, permitindo que você declare restrições comuns sem precisar escrever funções personalizadas. Entre elas estão required (obrigatório), min/max (intervalo numérico), minlength/maxlength (comprimento da string), enum (valores de enumeração), match (correspondência de expressão regular) e outras. Elas são aplicadas automaticamente para save() e validate() e servem como a primeira linha de defesa para a qualidade dos dados na camada de aplicação.

Como funciona: Antes de um documento ser salvo (ou quando validate() é chamado explicitamente), o Mongoose percorre o SchemaType de todos os campos e executa os validadores embutidos, um por um. Cada validador recebe o valor do campo e retorna um booleano ou lança um erro. Se a validação falhar, o Mongoose reúne todos os erros em um objeto ValidationError.errors, que inclui o caminho do campo, o tipo de erro e uma mensagem personalizada.

Comportamento implícito dos validadores embutidos: Os validadores embutidos apresentam vários comportamentos implícitos que podem passar despercebidos facilmente — 1. O validador required verifica se um campo existe e não é undefined, mas uma string vazia '' passa na verificação required (é preciso usar minlength: 1 para impedir strings vazias); 2. min e max funcionam apenas com o tipo Number (para tipos String, min e max são comparados por ordem lexicográfica, e não por valor numérico); 3. enum diferencia maiúsculas de minúsculas ('Admin' e 'admin' são valores diferentes); 4. match verifica apenas o formato, não o conteúdo (por exemplo, /\d+/ corresponde a “abc123def”; use /^d+$/ para uma correspondência estrita de dígitos puros).

Mensagens de erro personalizadas para validadores: Cada validador integrado suporta mensagens de erro personalizadas — required: [true, 'O nome de usuário não pode estar vazio'], min: [0, 'A idade não pode ser negativa'], enum: {values: ['customer', 'admin'], message: '{VALUE} não é uma função válida'}. Os modelos de mensagem suportam variáveis como {VALUE} (valor atual), {PATH} (nome do campo) e {MIN}/{MAX} (valores limite). Boas mensagens de erro devem permitir que os desenvolvedores front-end saibam como corrigir o problema sem consultar o esquema — “O nome de usuário deve ter de 3 a 30 caracteres alfanuméricos e sublinhados” é muito mais útil do que “Falha no validador para o campo nome de usuário”.

100%
graph TD
    A[doc.save] --> B[validate Phase]
    B --> C[Inspection required]
    B --> D[Inspection min/max]
    B --> E[Inspection enum]
    B --> F[Inspection match]
    B --> G[Inspection minlength/maxlength]
    
    C --> H{All passed?}
    D --> H
    E --> H
    F --> H
    G --> H
    
    H -->|Yes| I[pre save hooks]
    H -->|No| J[ValidationError<br/>Collect all errors]
    
    I --> K[MongoDB insertOne]
    
    style K fill:#d4edda
    style J fill:#f8d7da
Validador Tipo aplicável Condição de acionamento Modelo de mensagem de erro
required Todos Quando falta um campo '{PATH} is required'
min/max Número, Data Valor fora do intervalo '{PATH} must be >= {MIN}'
minlength/maxlength String Quando o comprimento é inválido '{PATH} must be >= {MINLENGTH} chars'
enum String Quando o valor não está na enumeração '{VALUE} is not valid'
match String Quando a expressão regular não corresponde '{PATH} is invalid'
unique Todos Quando há um conflito de índice E11000 duplicate key

(1) Lista completa

Prioridade e ordem de execução dos validadores: O Mongoose executa os validadores em uma ordem fixa — 1. Conversões de tipo integradas (String→Number, etc.); 2. Verificações obrigatórias; 3. Verificações de intervalo integradas (min/max/minlength/maxlength/enum/match); 4. Validadores síncronos personalizados; 5. Validadores assíncronos personalizados. Isso significa que, mesmo que um validador personalizado seja aprovado, um validador embutido ainda pode falhar. Princípio de design: priorize os validadores embutidos (melhor desempenho, mensagens de erro padronizadas); use validadores personalizados apenas em cenários não cobertos pelos validadores embutidos.

Criação de mensagens de erro para validadores personalizados: Uma boa mensagem de erro deve incluir três elementos — 1. Qual campo apresenta um erro (o Mongoose fornece automaticamente o caminho); 2. Por que o erro ocorreu (por exemplo, “O nome de usuário deve começar com uma letra”, em vez de “Falha na validação”); 3. Qual é o formato esperado (por exemplo, “Formato: deve começar com uma letra, 3 a 30 caracteres alfanuméricos ou sublinhados”). O message suporta variáveis de modelo: {PATH} (nome do campo), {VALUE} (valor atual), {MINLENGTH} (comprimento mínimo), etc. As mensagens de erro em produção devem permitir que os desenvolvedores front-end corrijam os problemas sem consultar a documentação — isso é mais valioso do que mensagens breves, mas vagas.

Validador Tipo aplicável Descrição
required Todos Campos obrigatórios
min/max Número, Data Intervalo numérico
minlength/maxlength String Comprimento da string
enum String Valores enumerados
match String Correspondência de expressão regular
unique Todos Índice exclusivo (camada de banco de dados)

▶ Exemplo 1: Uso prático do validador integrado

Análise das estratégias da camada de validação: Decidir em qual camada realizar a validação de dados é uma decisão arquitetônica crítica. A validação na camada de aplicação (validadores do Mongoose) é flexível e controlável — suporta lógica personalizada, validação assíncrona e mensagens de erro intuitivas —, mas só é eficaz para clientes Node.js. A validação na camada de banco de dados ($jsonSchema) é rigorosa e confiável — aplica-se a todos os clientes —, mas suporta apenas regras estáticas. A melhor estratégia é a validação dupla: use o Mongoose para a validação primária (regras de negócios, avisos fáceis de entender) e o $jsonSchema como plano B (proteção estrutural básica para impedir que a camada de aplicação seja contornada).

Ordem de execução das validações: As validações do Mongoose são executadas em uma ordem rígida: 1. Validações integradas do SchemaType (obrigatório → conversão de tipo → mínimo/máximo/comprimento mínimo/comprimento máximo/enumeração/correspondência); 2. Validadores síncronos personalizados; 3. Validadores assíncronos personalizados; 4. Middleware pre-validate; 5. Middleware pre-save. Uma falha em qualquer etapa interromperá as validações subsequentes e lançará uma exceção ValidationError.

Escolhendo entre validadores síncronos e assíncronos: Os validadores do Mongoose podem ser de dois tipos: síncronos e assíncronos. Os validadores síncronos retornam um booleano (por exemplo, validator: v => v.length >= 3), enquanto os validadores assíncronos retornam uma Promise (por exemplo, validator: async function(v) { const existing = await User.findOne({email: v}); return !existing; }). Diretrizes de seleção: 1. Use validadores síncronos para validações que não exijam uma consulta ao banco de dados (verificações de formato, verificações de intervalo, correspondência de expressões regulares); 2. Use validadores assíncronos para validações que exijam uma consulta ao banco de dados (verificações de exclusividade, verificações de integridade referencial). Observação: Os validadores assíncronos são de 10 a 100 vezes mais lentos do que os validadores síncronos (cada validação envolve uma consulta ao banco de dados), portanto, seu uso deve ser minimizado — as verificações de exclusividade podem ser substituídas por índices exclusivos (garantidos na camada do banco de dados, o que é mais confiável e eficiente do que consultas na camada de aplicação).

Padrões de validadores combinados: Vários validadores podem ser combinados para abranger diferentes cenários — 1. Obrigatório + Formato: required: [true, 'E-mail é obrigatório'] + match: [/^.+@.+$/, 'Formato de e-mail inválido'] (primeiro verifica se existe, depois verifica o formato); 2. Intervalo + Personalizado: min: [0, 'Não pode ser um número negativo'] + validator: v => v % 1 === 0 (primeiro verifica o intervalo, depois verifica se é um número inteiro); 3. Enumeração + Condição: enum: ['rascunho', 'publicado'] + um validador personalizado que verifica que “ao mudar de 'rascunho' para 'publicado', o campo 'conteúdo' não deve estar vazio”. A ordem dos validadores é importante — coloque required em primeiro lugar (para que as regras subsequentes sejam ignoradas se o campo estiver vazio), as verificações de formato no meio e as verificações de lógica de negócios por último.

Camada de verificação Localização Vantagens Desvantagens
Integrado ao Mongoose Camada de aplicação Sem necessidade de configuração, execução automática Limitado a regras comuns
Mongoose Custom Camada de aplicação Flexível, assíncrono Aumenta a base de código
$jsonSchema Camada de banco de dados Aplica-se a todos os clientes Apenas regras estáticas
JAVASCRIPT
const UserSchema = new mongoose.Schema({
  email: {
  username: {
    type: String,
    required: true,
    unique: true,
    minlength: [3, 'Username at least 3 chars'],
    maxlength: [30, 'Username at most 30 chars'],
    match: [/^[a-zA-Z0-9_]+$/, 'Only letters, numbers, underscores']
  },
  age: {
    type: Number,
    required: true,
    min: [0, 'Age cannot be negative'],
    max: [150, 'Age too large']
  },
  role: {
    type: String,
    enum: {
      values: ['customer', 'admin', 'moderator'],
      message: '{VALUE} is not a valid role'
    },
    default: 'customer'
  },
  passwordHash: {
    type: String,
    required: true,
    minlength: 60  // bcrypt Hash Length
  }
});

100%
sequenceDiagram
    participant App as Application Code
    participant Schema as mongoose Schema
    participant DB as MongoDB

    App->>Schema: User.create({email, age})
    Schema->>Schema: Verification required
    Schema->>Schema: Verification match (emailFormat)
    Schema->>Schema: Verification min/max (age)
    alt Verification Passed
        Schema->>DB: insertOne()
        DB-->>Schema: Success
        Schema-->>App: Back User Object
    else Verification Failed
        Schema-->>App: ValidationError
    end

3. Validador personalizado

Explicação do conceito: Quando os validadores embutidos não conseguem atender às regras de negócios (como “o nome de usuário não pode começar com um número” ou “lista negra de domínios de e-mail”), o Mongoose permite que você escreva funções de validação personalizadas nas definições dos campos. Os validadores personalizados são divididos em tipos síncronos e assíncronos: as funções síncronas retornam boolean, enquanto as funções assíncronas retornam Promise<boolean>. Ambos são configurados por meio da opção validate.

Como funciona: Os validadores personalizados são executados após a validação integrada. Os validadores síncronos recebem os valores dos campos e retornam true em caso de sucesso ou false em caso de falha. Os validadores assíncronos recebem os valores dos campos e retornam uma Promise: resolve(true) em caso de sucesso e resolve(false) em caso de falha. Em caso de falha, uma mensagem de erro é gerada usando o modelo especificado pela opção message. Observação: os validadores assíncronos aumentam o atraso em cada operação de salvamento.

Padrão de combinação de validadores: É possível combinar vários validadores — o Mongoose os executa na ordem em que são declarados, e a validação só é bem-sucedida se todos forem aprovados. Combinações comuns: 1. required + match (obrigatório e com formatação correta); 2. minlength + validação personalizada de força de senha (verifica tanto o comprimento quanto a complexidade); 3. min + validação personalizada de intervalo (por exemplo, para “taxa de desconto entre 0 e 1”, use min: 0 + max: 1; no entanto, para “o valor não pode ser 0”, é necessário um validador personalizado: validator: v => v !== 0). Ao combinar validadores, preste atenção à distinção entre as mensagens de erro — o modelo de mensagem independente de cada validador ajuda o front-end a identificar com precisão o campo problemático.

100%
graph LR
    A[doc.save] --> B[Built-in validators<br/>required/min/max/enum...]
    B --> C{Built-in Pass?}
    C -->|No| D[ValidationError]
    C -->|Yes| E[Custom validators<br/>Synchronize/Asynchronous]
    E --> F{Custom Pass?}
    F -->|No| D
    F -->|Yes| G[pre save hooks]
    G --> H[MongoDB write]
    
    style D fill:#f8d7da
    style H fill:#d4edda
Dimensão de comparação Validador síncrono Validador assíncrono
Método de definição validator: v => v >= 18 validator: async v => await check(v)
Velocidade de execução Rápida (sem E/S) Lenta (pode exigir uma consulta ao banco de dados)
Usos típicos Validação de formato, verificação de intervalo Validação de exclusividade, verificação de lista negra
Tratamento de erros Retornar false resolve(false)
Recomendações de desempenho Usar como prioridade Usar somente quando necessário

(1) Validador síncrono

Padrões de Design para Validadores Síncronos: Os validadores síncronos são adequados para validação puramente lógica — regras que não envolvem consultas ao banco de dados. Padrões comuns: 1. Validação de formato (endereços de e-mail não devem conter o símbolo “+”; nomes de usuário não devem começar com um número); 2. Validação de intervalo (idade >= 18; taxa de desconto entre 0 e 1); 3. Validação de comprimento (senha com pelo menos 8 caracteres; número de telefone com 11 dígitos); 4. Validação combinada (data de término ≥ data de início). Princípios de projeto: a função do validador deve retornar apenas um valor booleano (true em caso de sucesso, false em caso de falha) e não deve causar efeitos colaterais (não modificar this nem lançar exceções).

Personalização de mensagens de erro para validadores personalizados: Uma boa mensagem de erro deve permitir que os desenvolvedores identifiquem o problema rapidamente — validator: {validator: v => /^[a-z]/.test(v), message: 'O nome de usuário deve começar com uma letra minúscula (valor atual: {VALUE})'}. Os modelos de mensagem suportam {VALUE} (valor atual), {PATH} (nome do campo), {MIN}/{MAX} (valores limite) e {LENGTH} (comprimento atual). Exemplos de mensagens de erro em chinês: 'A idade deve estar entre {MIN} e {MAX}; valor atual {VALUE}', 'A senha deve ter pelo menos {MINLENGTH} caracteres; comprimento atual {LENGTH}'. Quanto mais específica for a mensagem, mais eficientes se tornam os testes de integração do front-end — “O formato do nome de usuário está incorreto” é 100 vezes mais útil do que “Falha no validador”.

A fronteira entre validadores e lógica de negócios: Os validadores do Mongoose devem validar apenas a validade dos dados em si (“Os dados estão corretos?”) e não devem incluir lógica de negócios (“A operação é permitida?”). Critérios para distinção: 1. Validação de dados (parte do esquema): formato de e-mail, comprimento da senha, intervalo de valores — essas regras não mudam de acordo com os cenários de negócios; 2. Lógica de negócios (pertence ao Controlador/Serviço): se um usuário tem permissão para fazer alterações, se o saldo é suficiente, se o estoque está disponível — essas regras variam de acordo com o cenário. Consequências de misturar os dois: alterações e redefinições de senha exigem regras de validação diferentes, mas o esquema possui apenas um único conjunto de validadores, resultando na omissão da validação do comprimento da senha ou em sua codificação rígida durante as redefinições de senha.

JAVASCRIPT
const UserSchema = new mongoose.Schema({
  email: {
    type: String,
    validate: {
      validator: function(v) {
        // Email addresses cannot contain + sign (Email addresses with tags are not accepted.)
        return !v.includes('+');
      },
      message: 'Email cannot contain + character'
    }
  },
  age: {
    type: Number,
    validate: {
      validator: function(v) {
        return v >= 18;
      },
      message: 'Must be at least 18 years old'
    }
  }
});

(2) validador assíncrono (Assíncrono)

Escolha entre validadores síncronos e assíncronos: Os validadores síncronos são adequados para cálculos na memória (comparações numéricas, correspondências de expressões regulares, verificações de enumeração), enquanto os validadores assíncronos exigem consultas ao banco de dados ou chamadas a APIs externas. Princípios de seleção — 1. Use validadores síncronos sempre que possível (melhor desempenho, sem efeitos colaterais); 2. Use validadores assíncronos apenas quando for necessária uma consulta ao banco de dados (por exemplo, verificação da exclusividade do nome de usuário, detecção de palavras sensíveis); 3. Sobrecarga de desempenho dos validadores assíncronos: cada operação save requer um await para a consulta ao banco de dados; durante operações em lote, N validações = N consultas ao banco de dados; 4. Alternativa aos validadores assíncronos: coloque as verificações de exclusividade no middleware pre-save (que pode ser otimizado para processamento em lote), em vez de usar um validador async para campos individuais.

Riscos e estratégias de mitigação para validadores assíncronos: Os validadores assíncronos acionam uma consulta ao banco de dados sempre que os dados são salvos, o que pode se tornar um gargalo de desempenho em cenários de alta concorrência. Estratégias de mitigação: 1. Utilize-os apenas quando necessário (por exemplo, verificações de exclusividade, verificações de lista negra); 2. Substitua as verificações assíncronas de exclusividade por índices exclusivos (mais eficientes na camada do banco de dados); 3. Reduza as validações não críticas a verificações periódicas em lote; 4. Armazene em cache os resultados de consultas frequentes (por exemplo, entradas da lista negra podem ser armazenadas em cache por 5 minutos).

Padrão de Composição de Validadores: Regras de negócios complexas geralmente exigem uma combinação de vários validadores — por exemplo, um nome de usuário deve atender simultaneamente aos seguintes critérios: não conter palavras sensíveis (assíncrono), não começar com um número (síncrono) e ter de 3 a 30 caracteres (embutido). O Mongoose executa todos os validadores sequencialmente; se o primeiro falhar, o processo é interrompido. Recomenda-se colocar as validações síncronas leves primeiro (para falhar rapidamente) e as validações assíncronas pesadas por último (para evitar E/S desnecessárias).

JAVASCRIPT
const UserSchema = new mongoose.Schema({
  username: {
    type: String,
    validate: {
      validator: async function(v) {
        // Check if the username contains sensitive words
        const banned = await BannedWords.findOne({ word: v });
        return !banned;
      },
      message: 'Username contains banned word'
    }
  },
  email: {
    type: String,
    validate: {
      validator: async function(v) {
        // Check if the email domain has been blocked
        const domain = v.split('@')[1];
        const blocked = await BlockedDomains.findOne({ domain });
        return !blocked;
      },
      message: 'Email domain is blocked'
    }
  }
});

(3) A opção validateBeforeSave

JAVASCRIPT
// === Default behavior:save Front Auto validate ===
const user = new User({ email: 'invalid' });
await user.save();  // ValidationError

// === Skip validate(Not recommended)===
const user = new User({ email: 'invalid' });
await user.save({ validateBeforeSave: false });

// === Manual validate ===
const user = new User({ email: 'invalid' });
try {
  await user.validate();
} catch (err) {
  console.error(err.message);  // ValidationError
}

4. Middleware na prática

Visão geral do conceito: Os middlewares do Mongoose são funções de hook que são acionadas automaticamente durante o ciclo de vida das operações de dados. Esta seção enfoca os padrões de middleware mais comumente usados no desenvolvimento prático: hash de senha (pré-salvar), gerenciamento de carimbos de data/hora (pré-salvar / carimbos de data/hora embutidos), exclusões temporárias (filtragem pré-busca + métodos de instância) e preenchimento automático (pré-busca + preenchimento). Esses padrões abrangem 80% dos casos de uso de middleware.

Como funciona: O middleware é registrado em um esquema, e o Mongoose o invoca automaticamente ao realizar as operações correspondentes. pre('save') é executado antes de uma operação de gravação e pode modificar os dados do documento; pre(/^find/) é executado antes de uma consulta e pode modificar as condições da consulta; post('save') é executado após uma operação de gravação e pode acionar efeitos colaterais (registro em log, notificações). O middleware é executado em cadeia, e cada função deve chamar next() para passar o controle.

100%
graph TB
    A[Middleware Patterns] --> B[Password Hash<br/>pre save<br/>isModifiedTesting]
    A --> C[Timestamp<br/>pre save / timestamps option]
    A --> D[Soft Delete<br/>pre findFilter<br/>softDeleteMethods]
    A --> E[Auto-fill<br/>pre find populate]
    
    B --> F["Only when changing the password<br/>Re-hash"]
    C --> G["Automatic Maintenance<br/>createdAt/updatedAt"]
    D --> H["Query Auto-Exclusion<br/>isDeleted: true"]
    E --> I["Query Autocomplete<br/>Related Documents"]
    
    style B fill:#d4edda
    style C fill:#cce5ff
    style D fill:#fff3cd
    style E fill:#e2d5f1
Padrão de middleware Condições de acionamento API principal Usos típicos
Hash da senha pré-salvamento isModified('password') Hash armazenado apenas quando a senha é alterada
Carimbo de data/hora pré-gravação / carimbos de data/hora isNew, Date.now Manter automaticamente os campos de data/hora
Exclusão temporária pre /^find/ this.find({isDeleted:{$ne:true}}) Excluir automaticamente itens excluídos da pesquisa
Preenchimento automático pré-busca this.populate(path) Carregando associação automática de consultas
Exclusão em cascata post findOneAndDelete Model.deleteMany() Limpar associações ao excluir o documento principal

(1) Middleware de hash de senhas

Princípios de projeto de segurança de senhas: O hash de senhas é a base de um sistema seguro. O bcrypt é a escolha padrão do setor — ele inclui sal embutido, um fator de custo ajustável (10–12) e resistência a ataques de GPU/ASIC. Pontos-chave do projeto: 1. Use isModified() para evitar o rehash a cada salvamento; 2. Por padrão, os campos de senha não são retornados usando select: false; 3. Comprimento de prova pré-hash (para impedir que uma senha de 60 caracteres contorne a exigência de minlength após o hash); 4. Use a versão assíncrona para evitar o bloqueio do loop de eventos.

JAVASCRIPT
UserSchema.pre('save', async function(next) {
  // Re-hash only when the password field is modified
  if (!this.isModified('passwordHash')) return next();

  try {
    const salt = await bcrypt.genSalt(10);
    this.passwordHash = await bcrypt.hash(this.passwordHash, salt);
    next();
  } catch (err) {
    next(err);
  }
});

(2) Middleware de registro de data e hora

Opção “timestamps” x Middleware Manual: A opção timestamps: true do Mongoose gerencia automaticamente createdAt e updatedAt, eliminando a necessidade de escrever manualmente um middleware de pré-gravação — essa é a abordagem recomendada. O middleware manual só deve ser usado quando houver requisitos específicos: 1. Nomes personalizados para campos de carimbo de data/hora (por exemplo, created_at em vez de createdAt); 2. Quando os carimbos de data/hora precisarem ser associados a um ID de usuário (por exemplo, updatedBy); 3. Quando for necessário acionar uma lógica adicional ao atualizar o carimbo de data/hora. Em 95% dos casos, basta usar timestamps: true.

JAVASCRIPT
// === mongoose Built-in timestamps option ===
const schema = new mongoose.Schema({...}, { timestamps: true });
// Auto-add createdAt/updatedAt

// === Custom Timestamp Middleware ===
schema.pre('save', function(next) {
  this.updatedAt = new Date();
  if (this.isNew) {
    this.createdAt = new Date();
  }
  next();
});

(3) Middleware de exclusão temporária

Decisão de arquitetura sobre exclusão temporária: A exclusão física (exclusão definitiva) é irreversível e viola os requisitos de conformidade de dados (por exemplo, o “direito ao esquecimento” do GDPR exige a anonimização, e não a exclusão). A exclusão suave implementa a exclusão lógica por meio do sinalizador isDeleted — o middleware pre-find filtra automaticamente os documentos excluídos, tornando o processo transparente para o código de negócios. Os principais motivos para a escolha da exclusão suave são: 1. Recuperabilidade dos dados (dados excluídos acidentalmente podem ser restaurados); 2. Requisitos de auditoria (manutenção do histórico de operações); 3. Integridade de referência (as referências de outros documentos permanecem intactas).

Custos de armazenamento e estratégias de limpeza para exclusão temporária: O custo da exclusão temporária é que os “dados zumbis” continuam ocupando espaço de armazenamento e de índice — 1. Aumento excessivo do armazenamento: Supondo que 30% dos dados sejam excluídos temporariamente, o tamanho da coleção primária aumenta em 43% (100 / 70 ≈ 1,43), e o índice aumenta de forma semelhante; 2. Impacto nas consultas: o middleware de pré-busca adiciona a condição isDeleted: {$ne: true} a todas as consultas; embora os índices estejam disponíveis, isso aumenta a complexidade das consultas; 3. Estratégia de limpeza: uma tarefa cron migra os dados que estão com exclusão temporária há mais de 90 dias para uma coleção de arquivo (excluindo fisicamente os registros da coleção principal); a coleção de arquivo é mantida por um ano antes de ser excluída permanentemente; 4. Conformidade com o GDPR: Quando um usuário solicita a exclusão, os campos de informações pessoais são substituídos por [REDACTED] (anonimizados), em vez de simplesmente serem marcados como isDeleted — isso atende à exigência legal de que os dados sejam “não identificáveis”, ao mesmo tempo em que os dados são mantidos para análise estatística.

Comparação entre modelos de implementação de exclusão suave:

Padrão Implementação Impacto na consulta Dificuldade de recuperação
Indicador booleano isDeleted: booleano filtragem automática pré-pesquisa basta definir como false
Carimbo de data/hora deletedAt: Data deletedAt: {$ne: null} campo não definido
Substituição de conteúdo conteúdo: '[Excluído]' Não é necessária filtragem O texto original não pode ser restaurado
JAVASCRIPT
// === Soft-Delete Field ===
const schema = new mongoose.Schema({
  isDeleted: { type: Boolean, default: false },
  deletedAt: Date,
  deletedBy: { type: mongoose.Schema.Types.ObjectId, ref: 'User' }
});

// === pre find Filter: Deleted ===
schema.pre(/^find/, function(next) {
  this.find({ isDeleted: { $ne: true } });
  next();
});

// === softDelete Instance Methods ===
schema.methods.softDelete = async function(deletedBy) {
  this.isDeleted = true;
  this.deletedAt = new Date();
  this.deletedBy = deletedBy;
  return await this.save();
};

// === restore Instance Methods ===
schema.methods.restore = async function() {
  this.isDeleted = false;
  this.deletedAt = undefined;
  this.deletedBy = undefined;
  return await this.save();
};

(4) Preenchimento automático do middleware

Compromissos de design do preenchimento automático: O preenchimento automático no pre find aprimora a experiência de desenvolvimento — os dados relacionados são recuperados automaticamente a cada consulta, eliminando a necessidade de chamar manualmente o populate em cada controlador. No entanto, essa conveniência tem um custo: 1. É realizada uma operação adicional de E/S a cada consulta (mesmo quando os dados relacionados não são necessários); 2. Chamadas aninhadas de populate levam ao problema de consulta N+1; 3. É difícil preencher dados seletivamente com base em cenários específicos. Abordagem recomendada: use setOptions() para acionar o preenchimento condicionalmente, em vez de depender do preenchimento automático incondicional.

Controle de desempenho para preenchimento automático: Problemas de desempenho relacionados ao preenchimento automático podem ser gerenciados das seguintes maneiras: 1. Preenchimento seletivo: preencher apenas quando necessário (acionado por req.query.populate=true, em vez do preenchimento automático padrão); 2. Lista de campos permitidos: o preenchimento automático preenche apenas campos-chave (por exemplo, author: 'username avatar', em vez de todas as informações do usuário); 3. lean + $lookup manual: substitua populate por lean() e pipelines de agregação para consultas de lista sensíveis ao desempenho; 4. Armazenar em cache os resultados de populate: para dados associados que raramente mudam (como avatares ou funções dos usuários), armazene em cache os resultados de populate no Redis. Melhor prática em produção: “Use o preenchimento automático apenas para eficiência no desenvolvimento, não para o desempenho em produção.”

JAVASCRIPT
// === Default populate Related Fields ===
UserSchema.pre('find', function(next) {
  this.populate({
    path: 'profileId',
    select: 'avatar bio'
  });
  next();
});

// === Conditions populate ===
UserSchema.pre('find', function(next) {
  if (this.options.includeOrders) {
    this.populate('orders');
  }
  next();
});

// Usage:
const user = await User.findById(userId);  // Automatic populate profileId
const userWithOrders = await User.findById(userId).setOptions({ includeOrders: true });

▶ Exemplo: Completo User Validação com Custom Validators (Difficulty ⭐⭐)

JAVASCRIPT
// Scene: ShopHub User Registration with comprehensive validation rules
const UserSchema = new mongoose.Schema({
  email: {
    type: String,
    required: [true, 'Email is required'],
    unique: true,
    lowercase: true,
    trim: true,
    match: [/^\S+@\S+\.\S+$/, 'Invalid email format']
  },
  username: {
    type: String,
    required: [true, 'Username is required'],
    unique: true,
    minlength: [3, 'Username must be at least 3 characters'],
    maxlength: [30, 'Username cannot exceed 30 characters'],
    validate: {
      validator: function(v) {
        return /^[a-zA-Z][a-zA-Z0-9_]*$/.test(v);
      },
      message: 'Username must start with a letter and contain only alphanumeric characters'
    }
  },
  password: {
    type: String,
    required: [true, 'Password is required'],
    minlength: [8, 'Password must be at least 8 characters'],
    validate: {
      validator: function(v) {
        return /^(?=.*[A-Z])(?=.*[a-z])(?=.*\d).+$/.test(v);
      },
      message: 'Password must contain at least one uppercase letter, one lowercase letter, and one digit'
    }
  },
  age: {
    type: Number,
    min: [13, 'Must be at least 13 years old'],
    max: [120, 'Age cannot exceed 120']
  },
  role: {
    type: String,
    enum: {
      values: ['customer', 'admin', 'moderator'],
      message: '{VALUE} is not a valid role'
    },
    default: 'customer'
  }
}, { timestamps: true });

// Test validation
try {
  await User.create({
    email: 'invalid-email',
    username: '123bad',
    password: 'weak',
    age: 10
  });
} catch (err) {
  console.log(err.name);  // ValidationError
  for (const field in err.errors) {
    console.log(`${field}: ${err.errors[field].message}`);
  }
}

Saída:

TEXT 📖 Somente leitura
ValidationError
email: Invalid email format
username: Username must start with um letter and contain only alphanumeric characters
password: Password must contain at least one uppercase letter, one lowercase letter, and one digit
age: Must be at least 13 years old

▶ Exemplo 2: Uma implementação completa de middleware

Padrão de Composição da Cadeia de Middleware: Em projetos reais, um único esquema normalmente registra vários componentes de middleware para formar uma “cadeia de processamento” completa. Princípios de composição: 1. Coloque o middleware de transformação de dados no início (por exemplo, converter endereços de e-mail para letras minúsculas, cortar strings) para garantir que as validações subsequentes recebam dados padronizados; 2. Coloque o middleware de validação no meio (por exemplo, verificar o comprimento da senha em texto simples antes do hash); 3. O middleware baseado em efeitos colaterais é colocado no final (registros de auditoria, notificações push), momento em que os dados já foram verificados como válidos. O middleware de tratamento de erros (versão de 4 parâmetros) serve como uma rede de segurança para capturar todas as exceções.

JAVASCRIPT
// === Complete User Model Middleware ===
UserSchema.pre('save', async function(next) {
  if (this.isModified('passwordHash') && !this.passwordHash.startsWith('$2b$')) {
    this.passwordHash = await bcrypt.hash(this.passwordHash, 10);
  }
  next();
});

UserSchema.pre(/^find/, function(next) {
  this.where({ isDeleted: { $ne: true } });
  next();
});

UserSchema.post('save', function(doc, next) {
  if (this.wasNew) {
    logger.info(`New user: ${doc.email}`);
  }
  next();
});

UserSchema.post('findOneAndDelete', function(doc) {
  if (doc) {
    // Cascading Deletion of Related Data
    Session.deleteMany({ userId: doc._id });
    Cart.deleteMany({ userId: doc._id });
  }
});

5. Mensagens de erro personalizadas

Explicação do conceito: O Mongoose permite personalizar mensagens de erro para cada validador e suporta variáveis de modelo (como {VALUE}, {PATH}, {MIN}), tornando as mensagens de erro mais fáceis de entender para o usuário. Existem duas maneiras de personalizar as mensagens: (1) sintaxe de array [validator, message]; (2) sintaxe de objeto { validator, message }. Recomendamos o uso da sintaxe de objeto, pois ela é mais flexível e pode incluir variáveis de modelo.

Suporte multilíngue para mensagens de erro: As mensagens de erro em aplicativos de produção devem oferecer suporte a vários idiomas — 1. Modelagem de mensagens: Defina as mensagens de erro como strings de modelo (por exemplo, 'validation.{PATH}.min') em vez de codificá-las diretamente em chinês ou inglês; 2. Substituição em tempo de execução: a camada do Controlador seleciona um pacote de idioma com base no cabeçalho Accept-Language e substitui as variáveis do modelo; 3. Personalização no nível do campo: use funções em vez de strings para o message de cada campo do esquema — message: (props) => i18n.t('validation.age.min', { value: props.value }); 4. Middleware unificado de formatação de erros: converta de forma consistente o ValidationError do Mongoose para o formato i18n dentro do middleware de tratamento de erros. Esse mecanismo permite que uma única API atenda usuários em todo o mundo.

Como funciona: Quando a validação falha, o Mongoose substitui os placeholders na mensagem por variáveis do modelo. {VALUE} é substituído pelo valor real, {PATH} é substituído pelo caminho do campo e {MIN}/{MAX} são substituídos pelos valores das restrições. Essas informações ajudam o front-end a exibir com precisão a causa do erro.

Variável do modelo Significado Exemplo de saída
{VALUE} Valor real de entrada 'Age must be at least 18, got 15'
{PATH} Caminho de campo 'email is required'
{MIN} / {MAX} Valor limite de restrição 'Age must be >= 0'
Restrição de comprimento
JAVASCRIPT
const UserSchema = new mongoose.Schema({
  email: {
    type: String,
    required: [true, 'Email is required'],
    match: [/\S+@\S+\.\S+/, 'Invalid email format: {VALUE}'],
    unique: true
  },
  age: {
    type: Number,
    min: [18, 'Age must be at least 18, got {VALUE}'],
    max: [150, 'Age cannot exceed 150']
  },
  password: {
    type: String,
    minlength: [8, 'Password must be at least 8 characters'],
    validate: {
      validator: function(v) {
        return /[A-Z]/.test(v) && /[0-9]/.test(v);
      },
      message: 'Password must contain uppercase and digit'
    }
  }
});

6. Tratamento de erros no validate

Princípios da arquitetura de tratamento de erros: ValidationError contém informações de erro para todos os campos (não apenas o primeiro), o que permite que o front-end exiba todos os problemas de validação de uma só vez. Percorra o objeto err.errors para recuperar os detalhes do erro de cada campo — field (caminho do campo), message (mensagem de erro), value (valor real) e kind (tipo de validador). Em produção, o ValidationError deve ser convertido para um formato unificado de resposta de erro, em vez de expor diretamente a estrutura interna do Mongoose.

Classificação de erros e estratégias de recuperação: Os três tipos de erros são tratados de maneira muito diferente — o ValidationError (4xx) indica um problema na entrada do usuário e deve especificar o campo exato que está causando o erro; o CastError (4xx) normalmente indica um erro de formato de ID e deve exibir a mensagem “Identificador de recurso inválido”; E11000 (4xx) indica um conflito de restrição de exclusividade e deve especificar o campo e o valor duplicados. Erros 5xx não devem revelar detalhes técnicos; em vez disso, devem retornar uniformemente “Serviço temporariamente indisponível” e acionar um alerta.

100%
graph TD
    A[ValidationError] --> B[errors Object]
    B --> C["errors.email<br/>ValidatorError<br/>message: 'Invalid email'"]
    B --> D["errors.age<br/>ValidatorError<br/>message: 'Age must be >= 18'"]
    B --> E["errors._id<br/>CastError<br/>message: 'invalid ObjectId'"]
    
    F[MongoServerError] --> G["code: 11000<br/>Unique Index Conflict"]
    
    style A fill:#f8d7da
    style F fill:#fff3cd
Tipo de erro Condições de acionamento Método de detecção
ValidationError Falha no validador err.name === 'ValidationError'
CastError Falha na conversão de tipo err instanceof mongoose.Error.CastError
E11000 Conflito de índice exclusivo err.code === 11000

Estratégia unificada para o tratamento de erros de validação: As aplicações em produção devem tratar os erros de validação do Mongoose de maneira consistente, em vez de repetir blocos try-catch em cada controlador — 1. Middleware de erros: No middleware de tratamento de erros do Express, converta uniformemente ValidationError em uma resposta 400 e extraia as informações de erro de cada campo para gerar mensagens de fácil compreensão para o usuário; 2. Tratamento de CastError: CastErrors (como um ObjectId inválido) devem ser mapeados para um código de status 400, em vez de 500 — um usuário que envia um ID com formato incorreto comete um erro do lado do cliente; 3. Tratamento de E11000: conflitos de índice único devem ser mapeados para um código de status 409 Conflict, acompanhado de uma mensagem amigável (“Este endereço de e-mail já está registrado”), em vez de expor o erro bruto do MongoDB; 4. Erros desconhecidos: todos os demais erros retornam um código de status 500 com uma mensagem genérica (sem expor detalhes internos). A vantagem desse tratamento unificado é que o front-end precisa apenas de um único conjunto de lógica de tratamento de erros, e o controlador do back-end não precisa se preocupar com a formatação dos erros.

Exibição de erros de validação no front-end: Os erros de validação devem ser específicos ao nível do campo — 1. Erros no nível do campo: Cada campo em err.errors possui seu próprio message; o front-end pode exibir uma mensagem de erro em vermelho abaixo do campo de entrada correspondente; 2. Prioridade de erros: erros required > erros type > erros de validação personalizados (exibir “Obrigatório” primeiro, depois “Formato inválido”); 3. Validação em tempo real: O front-end usa o Joi para pré-validação (fornecendo feedback instantâneo à medida que o usuário digita), enquanto a validação do Mongoose no back-end serve como última linha de defesa (já que a validação do front-end pode ser potencialmente contornada); 4. Localização das mensagens de erro: err.errors[field].message usa modelos em chinês/inglês e retorna o idioma correspondente com base no cabeçalho Accept-Language. A exibição precisa dos erros permite que os usuários identifiquem e corrijam rapidamente os problemas, em vez de se depararem com uma mensagem vaga como “Entrada inválida”.

JAVASCRIPT
// === Handling Validation Errors ===
try {
  await User.create({ email: 'invalid', age: 200 });
} catch (err) {
  if (err.name === 'ValidationError') {
    // Handling Field Errors
    for (const field in err.errors) {
      console.error(`${field}: ${err.errors[field].message}`);
    }
  }
}

// === mongoose Error Type ===
const mongoose = require('mongoose');

if (err instanceof mongoose.Error.ValidationError) {
  // Validation Error
}
if (err instanceof mongoose.Error.CastError) {
  // Type Conversion Error (e.g. ObjectId Format error)
}
if (err.code === 11000) {
  // Unique Index Conflict
}

❓ Perguntas Frequentes

P: O que é avaliado primeiro, required ou default? R: required é avaliado primeiro; em seguida, default é aplicado. Se nenhum valor for fornecido e houver um valor para default, o valor de default será usado; se houver required: true e nenhum valor for fornecido, será gerado um erro.

P: O que acontece quando um validador personalizado gera um erro? R: O Error.message gerado passa a ser a mensagem de erro para esse campo. Para personalizar o formato, use a opção message.

P: O validateBeforeSave: false é seguro? R: Não, não é seguro. Utilize-o apenas em situações em que os dados sejam confiáveis (como na migração de scripts). Desative-o em ambientes de produção.

P: O validador assíncrono apresenta baixo desempenho? R: Relativamente baixo, pois cada registro requer uma consulta assíncrona. Recomendamos implementar restrições de integridade durante a fase de projeto do esquema e usar o validador assíncrono apenas para validações remotas necessárias.


📖 Resumo


📝 Exercícios

  1. Exercício básico (⭐): Defina o esquema do usuário e aplique todos os validadores integrados (obrigatório, e-mail, idade mínima e máxima, enumeração de funções).
  2. Pergunta básica (⭐): Adicione um validador personalizado: os nomes de usuário não podem começar com um número.
  3. Exercício avançado (⭐⭐): Implemente o hash de senha (verificação isModified) usando o middleware pre-save.
  4. Exercício avançado (⭐⭐): Implemente a exclusão suave (usando o filtro preFind e o método de instância softDelete).
  5. Desafio (⭐⭐⭐): Modelar o registro completo do usuário (5 ou mais validadores + hash de senha + carimbo de data/hora + exclusão temporária + tratamento de erros).
Web-Tutorial.com

Equipe Técnica Web-Tutorial

Uma plataforma de tutoriais mantida por diversos desenvolvedores. Cada tutorial é escrito e revisado por profissionais da área correspondente. Trabalhamos para manter nosso conteúdo preciso e confiável — se encontrar algum problema, avise-nos.

100%