Ada logika yang harus berjalan pada setiap penulisan data: nama dipangkas spasi, slug diturunkan dari judul, password di-hash, waktu edit dicatat. Jika logika itu ditulis di setiap handler route, cepat atau lambat satu tempat terlewat dan datanya melenceng. Middleware Mongoose memindahkan logika tersebut ke dalam schema: fungsi terdaftar untuk event tertentu (save, findOneAndUpdate, dan lain-lain) dan berjalan otomatis di mana pun operasi itu dipanggil. Bab ini mempraktikkan normalisasi dan slug otomatis, lalu menghadapi aturan pentingnya: jenis middleware berbeda menerima this yang berbeda.
Hook Siklus Hidup dan Bentuk Penulisannya #
Middleware dideklarasikan dengan schema.pre(namaEvent, fn) (sebelum) dan schema.post(namaEvent, fn) (sesudah). Event yang paling sering dipakai:
| Event | Kapan berjalan | this di dalam fungsi |
|---|---|---|
save | sebelum/sesudah save() dan create() | document |
validate | sebelum/sesudah validasi | document |
findOneAndUpdate | sebelum query update dijalankan | objek Query |
deleteOne, findOneAndDelete | sebelum query hapus dijalankan | objek Query |
insertMany | sebelum penyisipan massal | array document |
Di Mongoose 9, hook ditulis sebagai fungsi sinkron atau async — tanpa parameter next:
// mdtest: skip — ilustrasi bentuk; implementasi lengkap ada di file di bawah
skema.pre("save", async function () {
this.slug = buatSlug(this.judul) // cukup selesai, atau await operasi async
})Normalisasi, Slug, dan Stempel Waktu Otomatis #
File berikut memasang tiga hook: pre("save") merapikan input dan menurunkan slug, post("save") mencatat penyimpanan, dan pre("findOneAndUpdate") menjaga aturan slug tetap berlaku pada jalur update:
// middleware-artikel.js — pre/post save dan query middleware findOneAndUpdate
const mongoose = require("mongoose")
const uri =
process.env.MONGODB_URI ??
"mongodb://127.0.0.1:27017/?directConnection=true"
const buatSlug = (teks) =>
teks.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "")
async function main() {
await mongoose.connect(uri, { dbName: "latihan_node_middleware" })
await mongoose.connection.db.dropDatabase()
const artikelSchema = new mongoose.Schema({
judul: { type: String, required: true },
slug: String,
penulisEmail: String,
ringkas: String,
diedit: Date, // diisi middleware, bukan opsi timestamps
})
// document middleware: this = document yang akan disimpan
artikelSchema.pre("save", async function () {
console.log("[pre save] this adalah document:", this.constructor.name)
this.judul = this.judul.trim()
if (this.penulisEmail) {
this.penulisEmail = this.penulisEmail.trim().toLowerCase()
}
this.slug = buatSlug(this.judul)
this.diedit = new Date()
})
artikelSchema.post("save", (doc) => {
console.log("[post save] tersimpan:", doc.slug)
})
// query middleware: this = Query, BUKAN document
artikelSchema.pre("findOneAndUpdate", async function () {
console.log("[pre findOneAndUpdate] this instanceof Query:", this instanceof mongoose.Query)
const pembaruan = this.getUpdate()
const judulBaru = pembaruan.$set?.judul
if (judulBaru) {
this.setUpdate({
...pembaruan,
$set: { ...pembaruan.$set, slug: buatSlug(judulBaru), diedit: new Date() },
})
}
})
const Artikel = mongoose.model("Artikel", artikelSchema)
const a = await Artikel.create({
judul: " Belajar MongoDB ",
penulisEmail: " [email protected] ",
ringkas: "Pengenalan document database",
})
console.log("1. judul:", JSON.stringify(a.judul), "| slug:", a.slug)
console.log(" email:", a.penulisEmail, "| diedit bertipe Date:", a.diedit instanceof Date)
const ubah = await Artikel.findOneAndUpdate(
{ slug: "belajar-mongodb" },
{ $set: { judul: "MongoDB untuk Aplikasi Nyata" } },
{ returnDocument: "after" }
)
console.log("2. setelah update — judul:", ubah.judul, "| slug:", ubah.slug)
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/middleware-mongoose/middleware-artikel.jsOutput yang Dihasilkan:
[pre save] this adalah document: model
[post save] tersimpan: belajar-mongodb
1. judul: "Belajar MongoDB" | slug: belajar-mongodb
email: [email protected] | diedit bertipe Date: true
[pre findOneAndUpdate] this instanceof Query: true
2. setelah update — judul: MongoDB untuk Aplikasi Nyata | slug: mongodb-untuk-aplikasi-nyataMari kita baca hasilnya. Judul " Belajar MongoDB " tersimpan bersih sebagai "Belajar MongoDB" — spasi di awal dan akhir dipangkas oleh pre("save") sebelum document menyentuh database. Slug belajar-mongodb dan email huruf kecil berasal dari hook yang sama, dan field diedit terisi Date tanpa pemanggil menulisnya. Baris [post save] menunjukkan hook setelah penyimpanan menerima doc yang baru tersimpan — tempat yang tepat untuk mencatat audit atau mengirim event. Pada findOneAndUpdate, hook mengambil isi pembaruan lewat this.getUpdate(), menambahkan slug dan diedit, lalu memasangnya kembali dengan this.setUpdate(...) — dan output membuktikan hasilnya: judul baru membawa slug baru. Semua itu terjadi tanpa satu baris pun di kode pemanggil, yang hanya mengirim judul mentah dan mengharapkan sisanya diurus schema.
Query Middleware: this Bukan Document #
Perhatikan dua baris log berbeda pada output: pre("save") melaporkan this sebagai model (class document), sedangkan pre("findOneAndUpdate") melaporkan this instanceof Query: true. Inilah pembagian fundamental di Mongoose: middleware document menerima document utuh yang bisa dimodifikasi langsung (this.judul = ...), sedangkan middleware query menerima objek Query — tidak ada document yang bisa diubah karena data lama bahkan mungkin belum dibaca. Hook query bekerja dengan filter (this.getFilter()) dan pembaruan (this.getUpdate()/this.setUpdate()).
Konsekuensinya praktis: save() dan create() menjalankan hook document, tetapi updateOne(), findByIdAndUpdate() (padanan findOneAndUpdate), dan deleteOne() menjalankan hook query — keduanya dunia yang terpisah. Aturan yang perlu berlaku di kedua jalur (seperti slug kita) harus didaftarkan pada kedua event. Pemisahan ini juga menjelaskan mengapa diedit pada contoh tidak memakai { timestamps: true }: opsi tersebut memang sudah mengurus createdAt/updatedAt di kedua jalur, jadi menulis stempel waktu manual seperti di atas hanya tepat bila kita butuh nama field, logika, atau peristiwa yang berbeda dari yang ditawarkan opsi bawaan.
Use Case dan Batasnya #
Empat pola yang paling umum di aplikasi nyata:
- Normalisasi input — trim, huruf kecil, format nomor telepon; murah dan selalu aman di
pre("save"). - Penurunan nilai — slug dari judul, total dari rincian item; hitung ulang di setiap jalur tulis agar tidak basi.
- Hash kredensial —
pre("save")meng-hash password dengannode:cryptoatau bcrypt; jaga agar hanya berjalan saat field berubah (this.isModified("password")) agar penyimpanan ulang tanpa ganti password tidak meng-hash ulang. - Stempel waktu dan audit —
dieditpada contoh kita adalah versi manual dari{ timestamps: true };posthook cocok untuk log audit dan notifikasi karena document sudah berada di database.
Rangkuman #
- Middleware dideklarasikan per event di schema; di Mongoose 9 penulisannya fungsi sinkron atau
asynctanpa callbacknext. pre/post("save")menerima document sebagaithis;pre("findOneAndUpdate")dan sesama query hook menerima objekQuery— ubah pembaruan lewatgetUpdate()/setUpdate().- Normalisasi, penurunan nilai, hash kredensial, dan stempel waktu adalah use case utama; daftarkan aturan yang sama pada event document dan event query bila keduanya dipakai.
posthook adalah tempat log audit dan efek samping setelah data benar-benar tersimpan.