Membangun CRUD lengkap dengan MongoDB dan Mongoose di Express 5: koneksi database, schema dan model, operasi create read update delete, serta pitfall ObjectId casting dan lean query.

Setelah di episode 8 struktur aplikasi modular berdiri, episode 9 menambahkan hal yang membuat API terasa nyata: database. Kita mulai dengan MongoDB lewat Mongoose — pilihan alami karena model berbasis dokumen JSON sangat dekat dengan objek JavaScript, dan Mongoose menyediakan validasi schema di level aplikasi.
Mengapa memakai Mongoose, bukan driver mongodb mentah? Mongoose menambahkan tiga hal yang menyelamatkan di produksi: schema yang mendefinisikan bentuk data, validation sebelum data masuk database, dan middleware (hooks) untuk logika seperti hashing password (episode 12). Semua ini membuat kontrak data bisa dijaga di kode, bukan hanya diharapkan.
Pastikan MongoDB lab berjalan (episode 0), lalu install driver:
npm install mongooseimport mongoose from "mongoose"
export async function connectDB(uri = process.env.MONGODB_URI) {
await mongoose.connect(uri, {
serverSelectionTimeoutMS: 5000,
})
console.log("MongoDB terhubung")
}
export default mongooseString koneksi default mongodb://localhost:27017/expresslab cocok dengan kontainer lab. Opsi serverSelectionTimeoutMS membuat koneksi gagal cepat alih-alih menggantung saat database tidak aktif.
Important
Panggil connectDB() di server.js sebelum app.listen(). Dengan cara ini server tidak menerima request saat database belum siap — kegagalan koneksi terdeteksi di awal, bukan sebagai request 500 di menit pertama.
import mongoose from "mongoose"
const userSchema = new mongoose.Schema(
{
name: { type: String, required: true, trim: true, maxlength: 100 },
email: {
type: String,
required: true,
unique: true,
lowercase: true,
match: /^[^\s@]+@[^\s@]+\.[^\s@]+$/,
},
age: { type: Number, min: 0, max: 150 },
tags: [{ type: String }],
},
{ timestamps: true }
)
export const User = mongoose.model("User", userSchema)Beberapa keputusan schema yang perlu dipahami:
trim: true membersihkan spasi di awal/akhir, lowercase: true menormalkan email — sanitasi di level model.unique: true membuat unique index di MongoDB, bukan validasi — duplikat tetap bisa muncul sebagai error duplicate key saat insert.timestamps: true otomatis menambah createdAt dan updatedAt — gratis tanpa kode.match melakukan validasi format email di level aplikasi sebelum sampai database.export const createUser = async (req, res) => {
const user = await User.create(req.body)
res.status(201).json({ data: user })
}User.create() memvalidasi data terhadap schema dan melempar ValidationError jika gagal — yang akan ditangkap error handler episode 7.
export const listUsers = async (req, res) => {
const users = await User.find({}).sort({ createdAt: -1 }).lean()
res.json({ data: users })
}
export const getUser = async (req, res) => {
const user = await User.findById(req.params.id).lean()
if (!user) throw new AppError(404, "USER_NOT_FOUND", "User tidak ditemukan")
res.json({ data: user })
}.lean() mengembalikan plain object alih-alih instance Mongoose — lebih cepat dan lebih ringan untuk response JSON. Tanpa itu, Mongoose mengembalikan dokumen lengkap dengan metode instance yang tidak perlu dikirim.
export const updateUser = async (req, res) => {
const user = await User.findByIdAndUpdate(
req.params.id,
req.body,
{ new: true, runValidators: true }
)
if (!user) throw new AppError(404, "USER_NOT_FOUND", "User tidak ditemukan")
res.json({ data: user })
}Dua opsi yang mudah terlewat:
new: true mengembalikan dokumen setelah update — tanpa itu, hasilnya dokumen lama.runValidators: true menjalankan validasi schema pada data baru. Tanpa itu, validasi dilewati pada update — celah data kotor.export const deleteUser = async (req, res) => {
const user = await User.findByIdAndDelete(req.params.id)
if (!user) throw new AppError(404, "USER_NOT_FOUND", "User tidak ditemukan")
res.status(204).end()
}res.status(204).end() mengirim respons tanpa body — konvensi REST untuk delete yang sukses.
Controllers dihubungkan ke routes seperti pola episode 8:
import { Router } from "express"
import {
createUser,
listUsers,
getUser,
updateUser,
deleteUser,
} from "../controllers/userController.js"
const router = Router()
router.get("/", listUsers)
router.get("/:id", getUser)
router.post("/", createUser)
router.put("/:id", updateUser)
router.delete("/:id", deleteUser)
export default routerfindById("abc") melempar CastError, bukan mengembalikan null:
export const getUser = async (req, res) => {
if (!mongoose.isValidObjectId(req.params.id)) {
throw new AppError(400, "INVALID_ID", "Format id tidak valid")
}
const user = await User.findById(req.params.id).lean()
if (!user) throw new AppError(404, "USER_NOT_FOUND", "User tidak ditemukan")
res.json({ data: user })
}Tanpa pengecekan ini, id acak menghasilkan 500 alih-alih 400. Periksa validitas ObjectId sebelum query — pola yang sama kita terapkan pada semua ID di episode 13.
Duplicate email menghasilkan error duplicate key (kode 11000), bukan ValidationError. Di produksi, petakan error ini ke 409 Conflict di error handler:
export const errorHandler = (err, req, res, next) => {
if (err.code === 11000) {
return res.status(409).json({ error: "DUPLICATE", message: "Data sudah ada" })
}
next(err)
}Update tanpa runValidators: true membiarkan data tidak valid masuk. Jadikan opsi ini kebiasaan tetap di semua operasi update.
Tip
Gunakan await User.countDocuments() untuk mengecek jumlah data, dan uji CRUD lengkap dengan curl dari episode 3. Kalian akan melihat seluruh alur — create → list → get → update → delete — bekerja melawan database sungguhan.
Episode 9 membangun CRUD MongoDB pertama yang utuh: koneksi dengan mongoose.connect, schema dengan validasi, operasi create/read/update/delete, dan penanganan pitfall seperti CastError dan duplicate key.
Inti yang harus dibawa pulang:
connectDB() dipanggil sebelum listen agar server siap sebelum menerima request.findByIdAndUpdate butuh { new: true, runValidators: true } untuk hasil dan validasi yang benar.isValidObjectId mencegah CastError berubah menjadi 500.Di episode 10 selanjutnya kita akan membangun CRUD dengan PostgreSQL — pool pg, Prisma dengan migration, dan perbandingan alur kerja relasional melawan MongoDB yang baru saja kita kuasai. Sampai jumpa di episode 10!