Server tahap CRUD mempercayai klien sepenuhnya — dan itu terbukti berbahaya. Coba kirim {"kode":"TG-003","judul":123}: respons 201 dan document tersimpan dengan judul berupa angka. Kirim body kosong: dijawab 500, padahal kesalahan jelas milik klien. Kirim kode yang sudah dipakai: 500 lagi, sementara console server penuh jejak galat duplikat E11000. Pada bagian ini kita memasang dua lapisan pertahanan: validasi payload sebelum data menyentuh database, dan satu format error yang seragam untuk semua kegagalan.
Format Error Seragam #
Setiap kegagalan kini berbentuk amplop yang sama — error berisi kode, pesan, dan opsional detail:
| Status | kode | Pemicu |
|---|---|---|
| 400 | VALIDASI | Payload melanggar skema |
| 400 | JSON_TIDAK_VALID | Body bukan JSON yang bisa diurai |
| 400 | ID_TIDAK_VALID | :id bukan 24 karakter heksadesimal |
| 400 | PAYLOAD_BESAR | Body melebihi 64 KB |
| 404 | TUGAS_TIDAK_ADA / ROUTE_TIDAK_ADA | Resource atau endpoint hilang |
| 409 | DUPLIKAT | Unique index menolak kode kembar (E11000) |
| 500 | KESALAHAN_SERVER | Galat tak terduga — satu-satunya 500 |
Pembagian tanggung jawabnya tegas: 400 berarti klien mengirim hal yang salah, 404 berarti halnya tidak ada, 409 berarti bentrok dengan data yang sudah ada, dan 500 berarti server kami yang bermasalah. Status 401 dan 403 menyusul di Bagian 4.
Validasi Manual Bergaya jsonSchema #
Kita menulis skema sendiri yang bentuk aturannya meniru $jsonSchema dari chapter JSON Schema Validation — wajib seperti required, tipe seperti bsonType, enum dan batas panjang seperti aslinya:
// validasi.js — validasi payload manual bergaya $jsonSchema
// Skema mendeklarasikan field yang dikenal beserta aturannya, lalu
// validasiPayload() memeriksa payload, membuang field tak dikenal,
// dan mengisi nilai bawaan. Hasilnya: { valid, galat, nilai }.
const STATUS = ["todo", "doing", "done"];
// Aturan untuk POST /tugas (pembuatan tugas baru).
const SKEMA_TUGAS = {
kode: { wajib: true, tipe: "string", panjangMin: 3, panjangMaks: 30 },
judul: { wajib: true, tipe: "string", panjangMin: 3, panjangMaks: 100 },
deskripsi: { wajib: false, tipe: "string", panjangMaks: 500 },
status: { wajib: false, tipe: "string", enum: STATUS, bawaan: "todo" },
};
// Aturan untuk PUT /tugas/:id — kode sengaja absen: kode tidak bisa diubah.
const SKEMA_UBAH = {
judul: { wajib: false, tipe: "string", panjangMin: 3, panjangMaks: 100 },
deskripsi: { wajib: false, tipe: "string", panjangMaks: 500 },
status: { wajib: false, tipe: "string", enum: STATUS },
};
function periksaField(nama, nilai, aturan, galat) {
const sebelum = galat.length;
if (nilai === undefined || nilai === null) {
if (aturan.wajib) galat.push({ field: nama, pesan: "field wajib diisi" });
return galat.length === sebelum;
}
if (typeof nilai !== aturan.tipe) {
galat.push({ field: nama, pesan: "harus bertipe " + aturan.tipe });
return false;
}
if (aturan.panjangMin && nilai.length < aturan.panjangMin) {
galat.push({ field: nama, pesan: "minimal " + aturan.panjangMin + " karakter" });
}
if (aturan.panjangMaks && nilai.length > aturan.panjangMaks) {
galat.push({ field: nama, pesan: "maksimal " + aturan.panjangMaks + " karakter" });
}
if (aturan.enum && !aturan.enum.includes(nilai)) {
galat.push({
field: nama,
pesan: "harus salah satu dari: " + aturan.enum.join(", "),
});
}
return galat.length === sebelum;
}
// Memeriksa payload terhadap skema; field tak dikenal dibuang diam-diam.
function validasiPayload(payload, skema) {
const galat = [];
const nilai = {};
for (const nama of Object.keys(skema)) {
const aturan = skema[nama];
const masuk = periksaField(nama, payload[nama], aturan, galat);
if (masuk && payload[nama] !== undefined && payload[nama] !== null) {
nilai[nama] = payload[nama];
} else if (masuk && aturan.bawaan !== undefined) {
nilai[nama] = aturan.bawaan;
}
}
return { valid: galat.length === 0, galat: galat, nilai: nilai };
}
// PUT wajib mengubah minimal satu field yang dikenal.
function validasiUbah(payload) {
const hasil = validasiPayload(payload, SKEMA_UBAH);
if (hasil.valid && Object.keys(hasil.nilai).length === 0) {
hasil.galat.push({ field: "payload", pesan: "tidak ada field yang dikenal" });
hasil.valid = false;
}
return hasil;
}
// Pembuatan tugas memakai SKEMA_TUGAS penuh.
function validasiTugas(payload) {
return validasiPayload(payload, SKEMA_TUGAS);
}
// Id MongoDB berupa 24 karakter heksadesimal.
function idValid(id) {
return typeof id === "string" && /^[0-9a-f]{24}$/.test(id);
}
module.exports = {
STATUS, SKEMA_TUGAS, SKEMA_UBAH,
validasiPayload, validasiTugas, validasiUbah, idValid,
};Empat hal patut dicermati:
periksaField()mengembalikan lolos-tidaknya field itu sendiri — penandasebelummemastikan galat field lain tidak ikut menular, sehingganilaihanya berisi field yang sehat.- Field tak dikenal dibuang, bukan ditolak.
validasiPayload()hanya menyalin field yang ada di skema;peranatau_idyang diselundupkan klien tidak pernah sampai ke repository. Inilah yang menutup celah mass assignment pada PUT tahap sebelumnya. SKEMA_UBAHtanpakode— daftar field yang bisa diubah ditentukan skema, sehinggakodetugas tidak bisa diganti setelah dibuat.idValid()memeriksa pola 24 karakter heksadesimal sebelum id dikirim keObjectId— tanpa gerbang ini, id sembarangan membuatnew ObjectId()melempar galat yang berujung 500.
Pemetaan Galat di Server #
server.js menambah class GalatApi untuk galat yang membawa status HTTP sendiri, serta petakanGalat() yang menerjemahkan galat JavaScript menjadi respons:
// Galat yang membawa kode status HTTP sendiri.
class GalatApi extends Error {
constructor(kodeStatus, kode, pesan) {
super(pesan);
this.kodeStatus = kodeStatus;
this.kode = kode;
}
}
// Menerjemahkan galat JavaScript/driver menjadi respons HTTP.
function petakanGalat(galat) {
if (galat instanceof GalatApi) {
return { status: galat.kodeStatus, isi: { kode: galat.kode, pesan: galat.message } };
}
if (galat instanceof SyntaxError) {
return { status: 400, isi: { kode: "JSON_TIDAK_VALID", pesan: "body bukan JSON yang valid" } };
}
if (galat.code === 11000) {
const field = Object.keys(galat.keyValue || {})[0] || "field";
return {
status: 409,
isi: { kode: "DUPLIKAT", pesan: field + " sudah dipakai tugas lain" },
};
}
return {
status: 500,
isi: { kode: "KESALAHAN_SERVER", pesan: "terjadi kesalahan tak terduga" },
};
}Handler POST kini memanggil validasiTugas(data) dan menolak hasil.valid === false dengan status 400 beserta detail; blok catch di akhir tangani() mengalihkan seluruh galat ke petakanGalat() dan hanya mencetak jejak lengkap ke console saat statusnya 500. Dua pemetaan menarik:
SyntaxError→ 400:JSON.parse("")melemparSyntaxError, sehingga body kosong atau rusak berhenti dianggap kesalahan server.galat.code === 11000→ 409: galat duplicate key dari driver membawakeyValueberisi field penyebab —{ kode: "TG-001" }— yang kita pakai untuk membuat pesan yang menjelaskan field mana yang bentrok.
Menguji Keempat Rambu #
Dengan validasi aktif, uji ulang payload bermasalah dari pembuka bagian ini:
curl -s -X POST http://127.0.0.1:3000/tugas \
-H "Content-Type: application/json" -d '{"kode":"TG-001","judul":"Menulis dokumen API"}'
curl -s -X POST http://127.0.0.1:3000/tugas -d '{"kode":"TG-100","judul":123}'
curl -s -X POST http://127.0.0.1:3000/tugas -d '{"kode":"TG-101","status":"selesai"}'Output yang Dihasilkan:
{
"_id": "6ad40d1a78a844804bb30190",
"kode": "TG-001",
"judul": "Menulis dokumen API",
"deskripsi": null,
"status": "todo",
"dibuat_pada": "2026-10-18T00:04:42.663Z",
"diubah_pada": "2026-10-18T00:04:42.663Z"
}
{
"error": {
"kode": "VALIDASI",
"pesan": "payload tidak valid",
"detail": [
{
"field": "judul",
"pesan": "harus bertipe string"
}
]
}
}
{
"error": {
"kode": "VALIDASI",
"pesan": "payload tidak valid",
"detail": [
{
"field": "judul",
"pesan": "field wajib diisi"
},
{
"field": "status",
"pesan": "harus salah satu dari: todo, doing, done"
}
]
}
}Payload ketiga menunjukkan detail berisi dua galat sekaligus — klien tahu persis field mana yang harus diperbaiki tanpa perlu bolak-balik. Lanjut ke tiga kasus lain:
curl -s -X POST http://127.0.0.1:3000/tugas
curl -s -X POST http://127.0.0.1:3000/tugas -d '{"kode":"TG-001","judul":"Duplikat kode"}'
curl -s http://127.0.0.1:3000/tugas/bukan-idOutput yang Dihasilkan:
{
"error": {
"kode": "JSON_TIDAK_VALID",
"pesan": "body bukan JSON yang valid"
}
}
{
"error": {
"kode": "DUPLIKAT",
"pesan": "kode sudah dipakai tugas lain"
}
}
{
"error": {
"kode": "ID_TIDAK_VALID",
"pesan": "id harus 24 karakter heksadesimal"
}
}Body kosong kini 400, kode kembar menjadi 409 DUPLIKAT (pemetaan E11000 bekerja), dan id tidak berbentuk ditolak sebelum menyentuh ObjectId. Terakhir, PUT yang hanya membawa field asing — termasuk upaya menimpa _id — ditolak karena skema tidak mengenalinya:
curl -s -X PUT http://127.0.0.1:3000/tugas/6ad40d1a78a844804bb30190 \
-H "Content-Type: application/json" \
-d '{"prioritas":"tinggi","_id":"000000000000000000000000"}'Output yang Dihasilkan:
{
"error": {
"kode": "VALIDASI",
"pesan": "payload tidak valid",
"detail": [
{
"field": "payload",
"pesan": "tidak ada field yang dikenal"
}
]
}
}Apa Berikutnya #
Data yang masuk kini bersih dan kegagalan berbicara dengan satu bahasa. Satu celah besar masih terbuka: siapa pun yang menemukan alamat server bisa menulis dan menghapus tugas. Bagian 4 memasang pintu berupa API key dan peran pengguna.