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
- Validadores integrados (obrigatório/mín./máx./enumeração/correspondência)
- Funções de validação personalizadas
- A opção
validateBeforeSave - validador assíncrono: validação assíncrona
- Middleware prático: hash de senha, exclusão temporária, preenchimento automático
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”.
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 |
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
}
});
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.
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.
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).
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
// === 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.
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.
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.
// === 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 |
// === 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.”
// === 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 ⭐⭐)
// 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 leituraValidationError 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.
// === 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 |
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.
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”.
// === 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,
requiredoudefault? R:requiredé avaliado primeiro; em seguida,defaulté aplicado. Se nenhum valor for fornecido e houver um valor paradefault, o valor dedefaultserá usado; se houverrequired: truee nenhum valor for fornecido, será gerado um erro.
P: O que acontece quando um validador personalizado gera um erro? R: O
Error.messagegerado passa a ser a mensagem de erro para esse campo. Para personalizar o formato, use a opçãomessage.
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
- Validadores integrados: required/min/max/enum/match/minlength/maxlength
- Validador personalizado: função síncrona ou assíncrona
- validateBeforeSave: Definido como true por padrão; pode ser desativado (não recomendado)
- pré-middleware: salvar/localizar/validar/remover
- middleware de post: save/findOneAndDelete
- Exclusão temporária: isDeleted + filtro de pré-busca
- Tratamento de erros: ValidationError, CastError, E11000
📝 Exercícios
- 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).
- Pergunta básica (⭐): Adicione um validador personalizado: os nomes de usuário não podem começar com um número.
- Exercício avançado (⭐⭐): Implemente o hash de senha (verificação isModified) usando o middleware
pre-save. - Exercício avançado (⭐⭐): Implemente a exclusão suave (usando o filtro
preFinde o método de instânciasoftDelete). - 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).