Di driver murni, collection apa pun bisa menerima document berbentuk apa pun — kebebasan yang berubah menjadi beban begitu dua fitur ditulis oleh dua orang pada dua hari berbeda. Schema Mongoose memutus masalah itu di sumbernya: satu deklarasi menentukan field apa yang dikenal, bertipe apa, dan apa nilai cadangannya. Bab ini merakit schema untuk entitas buku, mengubahnya menjadi model, lalu memeriksa apa yang benar-benar tersimpan di collection — termasuk tiga field yang tidak pernah kita tulis: _id, createdAt, dan updatedAt.

Skema Menentukan Bentuk Document #

Mongoose menyediakan SchemaType untuk semua tipe BSON yang lazim. Ringkasannya:

SchemaTypeUntukCatatan
String, Number, Booleanskalar dasardilengkapi casting otomatis
Datetanggal dan waktudisimpan sebagai BSON Date
mongoose.Schema.Types.ObjectIdreferensi antar documentbahasan bab Populate dan References
[String], [Number]array satu tipe[{}] untuk array subdocument
Mappasangan kunci-nilai dinamisalternatif object polos

Kita rakit schema buku lengkap dengan opsi collection yang mengunci nama collection — tanpa opsi ini, model Buku akan otomatis memakai collection bukus:

js
// skema-buku.js — schema lengkap, model, dan insert pertama
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_skema" })
  await mongoose.connection.db.dropDatabase()

  const bukuSchema = new mongoose.Schema(
    {
      kode: { type: String, required: true, unique: true },
      judul: { type: String, required: true },
      penulis: { type: [String], default: [] },
      harga: { type: Number, required: true, min: 0 },
      tersedia: { type: Boolean, default: true },
      terbit: { type: Date },
      stokDiperbarui: { type: Date, default: () => new Date() },
      penerbit: { type: mongoose.Schema.Types.ObjectId },
    },
    { timestamps: true, collection: "buku" }
  )

  const Buku = mongoose.model("Buku", bukuSchema)

  const buku = new Buku({
    kode: "BK-001",
    judul: "Laskar Pelangi",
    penulis: ["Andrea Hirata"],
    harga: 95000,
    terbit: new Date("2005-07-01"),
    rating: 5, // sengaja: field tak dikenal schema
  })
  console.log("1. _id sebelum save:", buku._id.constructor.name, "| isNew:", buku.isNew)
  console.log("2. field tak dikenal dibuang:", buku.rating)

  await Buku.init() // tunggu index (misalnya kode yang unik) selesai dibangun
  const tersimpan = await buku.save()
  console.log("3. createdAt:", tersimpan.createdAt.toISOString())
  console.log("4. updatedAt bertipe Date:", tersimpan.updatedAt instanceof Date)

  const mentah = await mongoose.connection.db.collection("buku").findOne({ kode: "BK-001" })
  console.log("5. field tersimpan:", Object.keys(mentah).join(", "))

  const indeks = await mongoose.connection.db.collection("buku").listIndexes().toArray()
  console.log("6. index:", indeks.map((i) => `${i.name}${i.unique ? " (unique)" : ""}`).join(", "))

  await mongoose.disconnect()
}

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/schemas-dan-models-mongoose/skema-buku.js

Output yang Dihasilkan:

text
1. _id sebelum save: ObjectId | isNew: true
2. field tak dikenal dibuang: undefined
3. createdAt: 2026-10-17T22:15:22.482Z
4. updatedAt bertipe Date: true
5. field tersimpan: _id, kode, judul, penulis, harga, tersedia, terbit, stokDiperbarui, createdAt, updatedAt, __v
6. index: _id_, kode_1 (unique)

Mari kita bedah hasilnya baris per baris:

  1. Baris 1 — buku._id sudah berupa ObjectId bahkan sebelum save(): Mongoose membuat _id di sisi klien saat document dibangun, bukan menunggu server. Flag isNew: true menandai document yang belum pernah disimpan.
  2. Baris 2 — rating: 5 tidak ada di schema, jadi langsung dibuang saat pembuatan document (perilaku strict bawaan). Schema bukan sekadar saran; ia filter aktif.
  3. Baris 3-4 — opsi { timestamps: true } menambahkan createdAt dan updatedAt otomatis pada setiap save, tanpa perlu kita menulis field tersebut.
  4. Baris 5 — isi collection memuat semua field schema plus tiga tambahan: _id, createdAt, updatedAt, dan __v (versionKey internal Mongoose). tersedia ikut tersimpan dengan nilai true dari default, dan stokDiperbarui terisi karena default berbentuk fungsi () => new Date() — dievaluasi saat document dibuat, bukan saat schema didefinisikan.
  5. Baris 6 — collection memiliki dua index: _id_ bawaan MongoDB dan kode_1 dengan tanda unique.

Tiga Nilai Otomatis yang Dikelola Mongoose #

Schema di atas memperlihatkan tiga mekanisme nilai otomatis yang berbeda sumbernya. Pertama, _id: dibangkitkan klien sebagai ObjectId dan dijamin unik lewat index _id_. Kedua, default: nilai statis (true, []) atau fungsi (() => new Date()) yang mengisi field yang tidak dikirim pemanggil. Ketiga, timestamps: opsi level schema yang mengisi createdAt sekali saat insert dan memperbarui updatedAt pada setiap operasi tulis berikutnya.

Ketiganya bekerja pada jalur document (save(), create()). Operasi update langsung seperti updateOne() melewati sebagian besar mekanisme ini — perilaku yang memengaruhi pilihan desain dan kita bahas lebih dalam di bab Middleware Mongoose.

Menata Model di Dalam Aplikasi #

Kembali ke struktur aplikasi bab pertama: tempat schema dan model adalah direktori src/models/, satu berkas per entitas. Berkas model mengekspor hasil mongoose.model() langsung sehingga lapisan service cukup meng-require dan memakainya:

js src/models/buku.js — pola satu berkas per model
// mdtest: skip — pola penataan berkas, bukan contoh yang dijalankan
const mongoose = require("mongoose")

const bukuSchema = new mongoose.Schema(
  { /* ...definisi field seperti di atas... */ },
  { timestamps: true, collection: "buku" }
)

module.exports = mongoose.model("Buku", bukuSchema)

Dua konvensi menjaga kode tetap sehat ketika model berjumlah banyak. Pertama, nama model ("Buku") harus unik per koneksi — Mongoose menyimpan model dalam registry, dan mendaftarkan nama sama dua kali dengan schema berbeda melempar galat OverwriteModelError; cukup require berkas yang sama alih-alih memanggil mongoose.model() ulang. Kedua, karena schema adalah satu-satunya tempat bentuk document didefinisikan, perubahan bentuk data selalu berawal dari berkas model — service tidak pernah menambahkan field baru diam-diam.

unique Adalah Index, Bukan Validasi #

Baris terakhir output di atas adalah pelajaran yang sering disalahpahami: unique: true pada schema menghasilkan index unik di server, bukan pemeriksaan di sisi aplikasi. Konsekuensinya besar. Index unik dijamin server pada saat insert — dua permintaan bersamaan dengan kode sama tetap akan ditolak satu di antaranya, sesuatu yang pemeriksaan “cari dulu, baru insert” di aplikasi tidak bisa janjikan. Sebaliknya, penolakan itu datang sebagai galat E11000 dari server, bukan sebagai ValidationError Mongoose.

Rangkuman #

  • Schema mendeklarasikan field, tipe, dan opsi; field di luar schema dibuang oleh mode strict, dan model dibuat dengan mongoose.model("Nama", skema, opsional: namaCollection).
  • _id dibangkitkan klien sebagai ObjectId, default bisa berupa nilai statis atau fungsi, dan { timestamps: true } mengelola createdAt/updatedAt.
  • unique: true membuat index unik di server — penegakan terjamin, tetapi kegagalannya datang sebagai galat E11000, bukan ValidationError.
  • Buku.init() menunggu index selesai dibangun sebelum operasi berikutnya bergantung padanya.