Input dari form pengguna, payload API, dan berkas impor tidak pernah bisa dipercaya apa adanya. Tanpa validasi, string kosong masuk sebagai nama, angka negatif menjadi harga, dan nilai yang tidak dikenal mengotori field status. Mongoose menempatkan pemeriksaan ini tepat di lapisan model: sekali dideklarasikan di schema, setiap save() dan create() melewatinya secara otomatis, dan galatnya terkumpul rapi dalam satu objek ValidationError. Bab ini memakai seluruh perangkat itu — validasi bawaan, custom validator, hingga batas terpentingnya: unique yang bukan validasi sama sekali.
Validasi Bawaan Schema #
Setiap SchemaType membawa validator bawaannya sendiri:
| Validator | Berlaku pada | Menolak |
|---|---|---|
required | semua tipe | nilai kosong (undefined, null, string kosong) |
enum | String, Number | nilai di luar daftar yang ditentukan |
min, max | Number, Date | nilai melewati batas |
match | String | string yang tidak cocok dengan regex |
minlength, maxlength | String | panjang di luar rentang |
Setiap validator menerima bentuk [aturan, "pesan"] sehingga pesan galat berbahasa domain kita, bukan pesan bawaan berbahasa Inggris. Untuk aturan yang tidak ada dalam daftar, schema menerima validate berisi fungsi validator dan pesan — inilah custom validator.
Custom Validator: Fungsi dan Pesan #
Fungsi validator menerima nilai field dan mengembalikan true bila lolos; pesan bisa berupa string atau fungsi yang menerima objek props (lengkap dengan props.value). Fungsi itu juga boleh async — contoh kita memeriksa ke database apakah sebuah email sudah dipakai, sesuatu yang tidak mungkin dinyatakan sebagai regex. Perhatikan bahwa validator custom ini menumpuk pada validator bawaan, bukan menggantikannya: field yang sama boleh menjalankan match untuk format lalu validate untuk ketersediaan, dan semuanya dilaporkan sebagai satu ValidationError jika beberapa di antaranya gagal:
email: {
type: String,
required: true,
match: [/.+@.+\..+/, "Format email tidak valid"],
unique: true,
validate: {
validator: async (v) =>
(await mongoose.models.Pelanggan.countDocuments({ email: v })) === 0,
message: (props) => `Email ${props.value} sudah terdaftar`,
},
},Ketiga lapisan pada field email di atas menjalankan peran berbeda: match memeriksa format, custom validator memeriksa ketersediaan, dan unique menegakkan larangan di server. Ketiganya berjalan berurutan pada jalur document.
Menangkap ValidationError #
File lengkap berikut menyusun schema pelanggan, menantangnya dengan document bermasalah, lalu mengamati bentuk galatnya:
// validasi-pelanggan.js — validasi bawaan, custom, async, dan galat E11000
const mongoose = require("mongoose")
const uri =
process.env.MONGODB_URI ??
"mongodb://127.0.0.1:27017/?directConnection=true"
async function main() {
await mongoose.connect(uri, { dbName: "latihan_node_validasi" })
await mongoose.connection.db.dropDatabase()
const pelangganSchema = new mongoose.Schema({
nama: {
type: String,
required: [true, "Nama wajib diisi"],
minlength: [3, "Nama minimal 3 karakter"],
},
email: {
type: String,
required: true,
match: [/.+@.+\..+/, "Format email tidak valid"],
unique: true,
validate: {
validator: async (v) =>
(await mongoose.models.Pelanggan.countDocuments({ email: v })) === 0,
message: (props) => `Email ${props.value} sudah terdaftar`,
},
},
peran: { type: String, enum: ["pembeli", "kasir", "admin"], default: "pembeli" },
umur: { type: Number, min: [18, "Umur minimal 18"], max: [100, "Umur maksimal 100"] },
situs: {
type: String,
validate: {
validator: (v) => v.startsWith("https://"),
message: (props) => `Situs ${props.value} harus diawali https://`,
},
},
})
const Pelanggan = mongoose.model("Pelanggan", pelangganSchema)
await Pelanggan.init() // index unik email siap sebelum insert pertama
// 1) banyak pelanggaran sekaligus, ditangkap tanpa save()
const bermasalah = new Pelanggan({
nama: "Bu",
email: "budi#example.com",
peran: "root",
umur: 15,
situs: "http://tokobuku.id",
})
try {
await bermasalah.validate()
} catch (err) {
console.log("1. galat:", err.name)
console.log(" field bermasalah:", Object.keys(err.errors).join(", "))
console.log(" pesan umur:", err.errors.umur.message)
console.log(" pesan situs:", err.errors.situs.message)
}
// 2) validateSync: sinkron, tapi usang di Mongoose 9
const ujiCepat = new Pelanggan({ nama: "Bu" })
const errSync = ujiCepat.validateSync()
console.log("2. validateSync:", errSync.name, "| field:", Object.keys(errSync.errors).join(", "))
// 3) data valid lolos, async validator memeriksa ke database
const budi = new Pelanggan({ nama: "Budi", email: "[email protected]", umur: 25, situs: "https://tokobuku.id" })
await budi.save()
console.log("3. tersimpan:", budi.nama, "| peran default:", budi.peran)
const duplikat = new Pelanggan({ nama: "Siti", email: "[email protected]", umur: 30 })
try {
await duplikat.save()
} catch (err) {
console.log("4. async validator menolak:", err.errors.email.message)
}
// 4) updateOne melewati validator document -> index unik yang menolak
await Pelanggan.create({ nama: "Dewi", email: "[email protected]", umur: 28 })
try {
await Pelanggan.updateOne({ nama: "Dewi" }, { $set: { email: "[email protected]" } })
} catch (err) {
console.log("5. E11000:", err.name, "| code:", err.code, "| keyValue:", JSON.stringify(err.keyValue))
}
await mongoose.disconnect()
}
main().catch((err) => {
console.error("GAGAL:", err.name, "-", err.message)
process.exit(1)
})cd /home/z/my-project/tutorial
MONGODB_URI="mongodb://127.0.0.1:27017/?directConnection=true" node content/mongodb/mongodb-dengan-nodejs/validation-mongoose/validasi-pelanggan.jsOutput yang Dihasilkan:
1. galat: ValidationError
field bermasalah: nama, email, peran, umur, situs
pesan umur: Umur minimal 18
pesan situs: Situs http://tokobuku.id harus diawali https://
2. validateSync: ValidationError | field: email, nama
3. tersimpan: Budi | peran default: pembeli
4. async validator menolak: Email [email protected] sudah terdaftar
5. E11000: MongoServerError | code: 11000 | keyValue: {"email":"[email protected]"}
(node:6479) [MONGOOSE] Warning: Mongoose: `Document.prototype.validateSync()` is deprecated and will be removed in Mongoose 10. Use `Document.prototype.validate()` instead.Bacaan per baris:
- Langkah 1 —
await doc.validate()menjalankan seluruh validator tanpa menyentuh database. Galatnya bertipeValidationError, dan propertierr.errorsadalah objek yang dikunci nama field:err.errors.umur.messagelangsung siap ditampilkan ke pengguna. Kelima field gagal sekaligus — validasi mengumpulkan semua pelanggaran, bukan berhenti di yang pertama. - Langkah 2 —
validateSync()memberi hasil serupa secara sinkron, tetapi Mongoose 9 menandainya usang (baris[MONGOOSE] Warningpada output; baris itu ditulis ke stderr sehingga posisinya bisa muncul di bagian mana pun) dan akan dihapus di Mongoose 10; gunakanawait validate(). Catatan penting lain:validateSync()melewatkan validator async seperti pemeriksaan email kita. - Langkah 3-4 — document valid tersimpan dengan
peranterisi default. Percobaan kedua dengan email yang sudah ada ditolak custom validator async dengan pesan ramah — inilah nilai tambah validator atas biaya satu querycountDocumentstambahan. - Langkah 5 —
updateOne()tidak menjalankan validator document secara bawaan, sehingga permintaan lolos ke server dan ditolak index unik dengan galatE11000(err.codedanerr.keyValueterisi).
unique Bukan Validasi: Tangani E11000 #
Bab schema sudah menandainya; di sini buktinya. unique tidak pernah mengisi err.errors karena ia bukan validator — ia index di server. Celahnya dua lapis: pertama, validator async dan insert adalah dua langkah terpisah, sehingga dua permintaan bersamaan sama-sama bisa lolos pemeriksaan “email belum terdaftar” sebelum salah satunya tersimpan; kedua, operasi seperti updateOne() memang tidak menjalankan validator document secara bawaan. Opsi { runValidators: true } bisa mengaktifkannya untuk update, tetapi pilihan itu tetap tidak menutup celah yang pertama — jarak waktu antara pemeriksaan dan penulisan tidak bisa dihilangkan dari sisi aplikasi.
Satu-satunya penegak yang bebas dari kedua celah itu adalah index unik. Karena itu pola yang benar adalah berlapis: validator untuk pesan galat yang ramah, index unik untuk jaminan terakhir, dan penanganan E11000 eksplisit di aplikasi:
// mdtest: skip — pola penanganan, Pelanggan berasal dari file di atas
try {
await Pelanggan.updateOne({ nama: "Dewi" }, { $set: { email: "[email protected]" } })
} catch (err) {
if (err.code === 11000) {
const field = Object.keys(err.keyValue)[0] // "email"
// terjemahkan menjadi pesan pengguna, mis. "email sudah dipakai"
}
throw err
}Rangkuman #
- Validator bawaan (
required,enum,min/max,match,minlength) dinyatakan di schema dengan opsi pesan khusus; custom validator menambah aturan apa pun lewatvalidate. await doc.validate()memeriksa document tanpa menyimpan;err.errorsberisi galat per field.validateSync()bersifat usang di Mongoose 9 dan melewatkan validator async.- Validator async bisa memeriksa kondisi di database, tetapi tidak menggantikan index unik — hasilnya bisa kedaluwarsa seketika.
uniqueadalah index server, bukan validasi: selalu tangani galatE11000(cekerr.codedanerr.keyValue) untuk pesan yang ramah pengguna.