Bab sebelumnya menyiapkan kerangka direktori; sekarang kita mengisi lapisan datanya dengan driver resmi mongodb. Pertanyaan pertama setiap proyek nyata hampir selalu sama: di mana koneksi dibuat, dan bagaimana bagian lain aplikasi memakainya tanpa saling menimpa? Jawabannya satu modul kecil bernama db.js yang mengekspor koneksi tunggal, lalu seluruh file lain tinggal memanggilnya. Bab ini membedah modul tersebut, membuktikan mekanisme singleton di baliknya, lalu menjalankan alur CRUD lengkap dan skenario koneksi gagal.

Modul db.js sebagai Pusat Koneksi #

Semua pengetahuan tentang cara terhubung — URI, nama database, timeout — dikunci dalam satu berkas. Modul ini yang akan di-require oleh setiap model di bab-bab selanjutnya:

js
// db.js — modul koneksi MongoDB tunggal untuk seluruh aplikasi
const { MongoClient } = require("mongodb")

const uri =
  process.env.MONGODB_URI ??
  "mongodb://127.0.0.1:27017/?directConnection=true"
const dbName = process.env.APP_DB ?? "latihan_node_driver"

let client = null

async function connect() {
  if (client) return client // idempoten: client sama untuk semua pemanggil
  client = new MongoClient(uri, { serverSelectionTimeoutMS: 5000 })
  await client.connect()
  await client.db("admin").command({ ping: 1 }) // pastikan server benar-benar menjawab
  return client
}

function getDb() {
  if (!client) {
    throw new Error("connect() belum dipanggil — getDb() menolak bekerja")
  }
  return client.db(dbName)
}

async function close() {
  if (client) {
    await client.close()
    client = null
  }
}

module.exports = { connect, getDb, close }

Empat keputusan penting pada modul di atas:

  1. let client = null di ruang lingkup modul — variabel ini hidup sekali per proses dan dibagi ke semua pemanggil require("./db"); inilah status yang menjaga koneksi tunggal.
  2. connect() idempoten — pemanggilan kedua langsung mengembalikan client yang sudah ada, jadi beberapa bagian aplikasi boleh memanggil connect() tanpa takut membuka pool baru.
  3. ping setelah connect() — connect() sukses hanya berarti driver mengenal topologi server; ping memastikan server benar-benar menjawab perintah sebelum aplikasi menyatakan diri siap.
  4. getDb() melempar galat yang jelas — lebih baik gagal dengan pesan eksplisit daripada TypeError misterius ketika client masih null.

Singleton lewat Cache require #

Bagaimana satu modul bisa dijamin tunggal padahal di-require dari banyak file? Mekanismenya ada di runtime Node.js: hasil eksekusi sebuah berkas disimpan di cache require dengan kunci berupa path absolut berkas itu, dan pemanggilan berikutnya untuk berkas yang sama menerima objek hasil cache yang identik — berkas tidak dieksekusi dua kali. Variabel client di db.js karena itu hanya pernah ada satu salinan, dan baris if (client) return client di dalam connect() menjadi pengaman kedua. Perilaku yang sama berlaku pada import ESM: spesifikasi modul ESM juga mengevaluasi setiap modul sekali lalu membagikan hasilnya.

Cache ini juga menjelaskan satu jebakan: menghapus cache lewat delete require.cache[...] untuk memaksa modul dievaluasi ulang akan membuat client kedua — pola yang kadang dipakai di pengujian untuk mengisolasi state, tetapi berbahaya di aplikasi karena membuka pool koneksi baru yang tidak pernah ditutup oleh pemanggil awal.

Alur CRUD dari Modul Aplikasi #

File konsumen berikut memakai db.js untuk satu siklus hidup pesanan: sisipkan, ubah status, baca ulang, hapus. Baris pertamanya sekaligus membuktikan janji singleton:

js
// pesan-pesanan.js — alur kerja CRUD lengkap lewat modul db.js
const { connect, getDb, close } = require("./db")

async function main() {
  // require() men-cache modul: dua panggilan mengembalikan objek yang sama
  const dbA = require("./db")
  const dbB = require("./db")
  console.log("1. modul hasil require identik:", dbA === dbB)

  await connect()
  const db = getDb()
  const pesanan = db.collection("pesanan")
  await pesanan.deleteMany({}) // mulai dari keadaan bersih

  const hasil = await pesanan.insertOne({
    pelanggan: "Budi",
    items: [{ kode: "BK-001", judul: "Laskar Pelangi", qty: 1 }],
    total: 95000,
    status: "baru",
  })
  console.log("2. insertOne acknowledged:", hasil.acknowledged, "| _id:", hasil.insertedId)

  const ubah = await pesanan.updateOne(
    { _id: hasil.insertedId },
    { $set: { status: "diproses" } }
  )
  console.log("3. updateOne matched:", ubah.matchedCount, "| modified:", ubah.modifiedCount)

  const doc = await pesanan.findOne(
    { _id: hasil.insertedId },
    { projection: { _id: 0, pelanggan: 1, status: 1, total: 1 } }
  )
  console.log("4. findOne:", JSON.stringify(doc))

  const hapus = await pesanan.deleteOne({ _id: hasil.insertedId })
  console.log("5. deleteOne deletedCount:", hapus.deletedCount)

  await close()
  console.log("6. koneksi ditutup, proses boleh berakhir")
}

main().catch((err) => {
  console.error("GAGAL:", err.name, "-", err.message)
  process.exit(1)
})
bash
cd /home/z/my-project/tutorial
MONGODB_URI="mongodb://127.0.0.1:27017/?directConnection=true" node content/mongodb/mongodb-dengan-nodejs/menggunakan-mongodb-driver/pesan-pesanan.js

Output yang Dihasilkan:

text
1. modul hasil require identik: true
2. insertOne acknowledged: true | _id: new ObjectId('6ad3f379fc0e52ee38eef7bc')
3. updateOne matched: 1 | modified: 1
4. findOne: {"pelanggan":"Budi","total":95000,"status":"diproses"}
5. deleteOne deletedCount: 1
6. koneksi ditutup, proses boleh berakhir

Baris demi baris: langkah 1 membuktikan dbA === dbB — kedua require menerima objek ekspor yang sama dari cache. Langkah 2 menyisipkan document pesanan dan menyimpan insertedId (bertipe ObjectId) sebagai pegangan untuk operasi berikutnya. Langkah 3 mengubah satu field lewat $set, dan dua penghitungnya (matchedCount, modifiedCount) mengonfirmasi satu document cocok lalu berubah. Langkah 4 membaca ulang dengan projection agar keluaran ringkas. Langkah 5 menghapus berdasarkan _id, dan langkah 6 menutup client — penting untuk skrip sekali jalan, karena socket yang terbuka membuat proses Node.js tetap hidup.

Kegagalan Koneksi yang Terlihat Jelas #

Aplikasi yang baik gagal dengan suara, bukan diam. Kita uji db.js dengan URI ke port yang tidak ada servernya:

bash
MONGODB_URI="mongodb://127.0.0.1:28099/?directConnection=true" node content/mongodb/mongodb-dengan-nodejs/menggunakan-mongodb-driver/pesan-pesanan.js

Output yang Dihasilkan (setelah menunggu serverSelectionTimeoutMS selama 5 detik):

text
1. modul hasil require identik: true
GAGAL: MongoServerSelectionError - connect ECONNREFUSED 127.0.0.1:28099

Galat MongoServerSelectionError muncul karena driver tidak menemukan satu pun server yang layak dalam batas waktu seleksi — penyebab umumnya URI salah, server mati, atau firewall memblokir port. Penanganan main().catch(...) mengubahnya menjadi satu baris pesan yang jelas dan kode keluar 1, bentuk kegagalan yang mudah dilihat di log maupun CI. Perhatikan bahwa langkah 1 tetap tercetak sebelum galat: kegagalan koneksi terjadi pada connect(), bukan saat memuat modul, sehingga cache singleton bekerja terlepas dari kondisi jaringan.

Rangkuman #

  • Satu modul db.js mengunci URI, nama database, dan timeout; mengekspor connect(), getDb(), dan close().
  • Cache require (dan evaluasi modul ESM sekali) menjadikan objek ekspor tunggal secara alami — require("./db") dari file mana pun mengembalikan objek yang sama.
  • connect() yang idempoten plus ping memastikan aplikasi menyatakan siap hanya setelah server menjawab.
  • Kegagalan koneksi sebaiknya muncul sebagai MongoServerSelectionError yang jelas saat boot, bukan kejutan di permintaan pertama.