Error handling pada Chapter sebelumnya menjawab permintaan yang tidak bisa dibaca. Sekarang kita menghadapi permintaan yang bisa dibaca tetapi tidak layak diproses: judul tugas berisi string kosong, status berisi kata yang tidak dikenal, atau field wajib yang tidak dikirim sama sekali. Prinsipnya sederhana — data dari client tidak pernah boleh dipercaya tanpa diperiksa — dan satu-satunya tempat pemeriksaan yang aman adalah di server, karena client selalu bisa dilewati orang dengan curl atau Postman. Pada Chapter ini kita membangun lapisan validasi manual untuk API tugas: memeriksa tipe, kerapian, panjang, dan nilai yang diizinkan, lalu mengembalikan seluruh temuan dalam satu response 422.
Memisahkan Dua Jenis Kesalahan Input #
Kesalahan input datang dalam dua rasa yang harus dibedakan, karena penanganannya berbeda:
| Jenis | Contoh | Status | Pesan untuk client |
|---|---|---|---|
| Format rusak | Body bukan JSON valid | 400 | Kirim ulang body yang benar-benar JSON |
| Isi tidak lolos aturan | title hanya 2 karakter, status tidak dikenal | 422 | Perbaiki nilai field tertentu |
Pemisahan ini mengikuti konvensi pada Chapter sebelumnya: 400 berarti client salah mengirim, 422 berarti client salah mengisi. Dengan kode yang berbeda, aplikasi client bisa menampilkan pesan yang tepat — “periksa koneksi atau serializer-mu” versus “perbaiki isian formulir ini”.
Aturan Validasi untuk Tugas #
API tugas kita memakai empat aturan yang mewakili pola validasi paling umum:
- Tipe:
titlewajib berupa string — angka, boolean,null, maupun field yang tidak ada, semuanya ditolak. - Kerapian:
titledipangkas spasi awal-akhirnya (trim) sebelum diperiksa, sehingga" ab "dihitung dua karakter, bukan enam. - Panjang: setelah dipangkas,
titleharus 3-80 karakter. - Daftar nilai:
statusbersifat opsional, tetapi bila diisi harus salah satu daribelumatauselesai.
Field di luar keduanya (misalnya prioritas) diam-diam diabaikan — kebijakan yang ramah untuk evolusi API; beberapa API memilih menolak field asing, dan keduanya sah asal konsisten.
Fungsi validasiTugas dan Alurnya di Handler #
Seluruh aturan di atas terkumpul dalam satu fungsi murni bernama validasiTugas: fungsi ini menerima input apa pun, mengembalikan array berisi daftar galat, dan tidak pernah melempar error. Handler POST tinggal memanggilnya setelah body berhasil diurai:
import http from 'node:http';
const host = '127.0.0.1';
const tugas = new Map([
[1, { id: 1, title: 'Belajar Node.js', status: 'selesai' }],
]);
let idBerikutnya = 2;
const STATUS_VALID = ['belum', 'selesai'];
function kirimJson(res, kode, data) {
res.writeHead(kode, { 'Content-Type': 'application/json' });
res.end(JSON.stringify(data));
}
function kirimError(res, kode, kodeError, pesan, detail) {
const error = { kode: kodeError, pesan };
if (detail) error.detail = detail;
kirimJson(res, kode, { error });
}
async function bacaBodyJson(req) {
const potongan = [];
for await (const data of req) potongan.push(data);
const teks = Buffer.concat(potongan).toString('utf8');
return JSON.parse(teks);
}
function validasiTugas(input) {
const galat = [];
if (typeof input !== 'object' || input === null) {
return ['body wajib berupa object JSON'];
}
if (typeof input.title !== 'string') {
galat.push('title wajib diisi berupa teks');
} else {
const judul = input.title.trim();
if (judul.length < 3) galat.push('title minimal 3 karakter');
if (judul.length > 80) galat.push('title maksimal 80 karakter');
}
if (input.status !== undefined && !STATUS_VALID.includes(input.status)) {
galat.push('status harus salah satu dari: belum, selesai');
}
return galat;
}
const server = http.createServer(async (req, res) => {
const url = new URL(req.url, `http://${host}`);
if (req.method === 'POST' && url.pathname === '/tugas') {
let body;
try {
body = await bacaBodyJson(req);
} catch {
return kirimError(res, 400, 'JSON_TIDAK_VALID', 'Body bukan JSON yang valid');
}
const galat = validasiTugas(body);
if (galat.length > 0) {
return kirimError(res, 422, 'VALIDASI_GAGAL', 'Data tugas tidak valid', galat);
}
const id = idBerikutnya++;
const baru = { id, title: body.title.trim(), status: body.status ?? 'belum' };
tugas.set(id, baru);
res.writeHead(201, { 'Content-Type': 'application/json', Location: `/tugas/${id}` });
return res.end(JSON.stringify(baru));
}
if (req.method === 'GET' && url.pathname === '/tugas') {
return kirimJson(res, 200, [...tugas.values()]);
}
return kirimError(res, 404, 'TIDAK_ADA', 'Endpoint tidak ditemukan');
});
server.listen(0, host, async () => {
const port = server.address().port;
const base = `http://${host}:${port}`;
async function kirim(label, body) {
const r = await fetch(`${base}/tugas`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body,
});
console.log(`POST /tugas ${label} -> ${r.status}`, JSON.stringify(await r.json()));
}
await kirim('#1', JSON.stringify({ title: 'ab', status: 'proses' }));
await kirim('#2', JSON.stringify({ status: 'selesai' }));
await kirim('#3', JSON.stringify({ title: ' Belajar validasi input ', status: 'selesai' }));
const daftar = await fetch(`${base}/tugas`);
console.log(`GET /tugas -> ${daftar.status}`, JSON.stringify(await daftar.json()));
server.close();
server.closeAllConnections();
});Output yang Dihasilkan:
POST /tugas #1 -> 422 {"error":{"kode":"VALIDASI_GAGAL","pesan":"Data tugas tidak valid","detail":["title minimal 3 karakter","status harus salah satu dari: belum, selesai"]}}
POST /tugas #2 -> 422 {"error":{"kode":"VALIDASI_GAGAL","pesan":"Data tugas tidak valid","detail":["title wajib diisi berupa teks"]}}
POST /tugas #3 -> 201 {"id":2,"title":"Belajar validasi input","status":"selesai"}
GET /tugas -> 200 [{"id":1,"title":"Belajar Node.js","status":"selesai"},{"id":2,"title":"Belajar validasi input","status":"selesai"}]Skenario uji di bagian bawah file mengirim tiga POST dengan kualitas berbeda lalu memeriksa hasil akhirnya. Mari kita bedah bagian-bagian pentingnya:
- Baris 9 (
STATUS_VALID): Daftar nilai yang diizinkan didefinisikan sekali sebagai array. Menambah status baru kelak cukup mengubah satu baris ini — tidak ada daftar nilai yang ditulis ulang di dalam handler. - Baris 16-20 (
kirimErrordengandetail): Bentuk error seragam dari Chapter sebelumnya diperluas dengan fielddetailyang hanya hadir untuk 422. Client yang tidak mengenaldetailtetap bisa membacakodedanpesan. - Baris 32-34 (pemeriksaan bentuk paling awal): JSON yang valid belum tentu object —
"42",true, dannullsemuanya JSON sah. Pemeriksaantypeof input !== 'object' || input === nullmenangkap semuanya sebelum kode sempat membacainput.titledan gagal. - Baris 36-42 (tipe, kerapian, panjang): Operator
typeofmemastikantitleberupa string — pemeriksaan ini sekaligus menolaknull, karenatypeof nullbernilai"object", bukan"string". Pemangkasantrim()dilakukan lebih dulu agar batas panjang dihitung dari isi sebenarnya; batas bawah dan atas diperiksa sebagai dua galat terpisah agar pesannya spesifik. - Baris 44-46 (nilai enum):
statusyang tidak diisi dibiarkan lolos karena field ini opsional;undefinedadalah satu-satunya nilai yang dikecualikan, dan nilai default"belum"diterapkan kemudian di handler. - Baris 54-71 (alur handler
POST): Urutannya kaku — baca body (gagal → 400), validasi (ada galat → 422), baru simpan (201). Data hanya menyentuh penyimpanan setelah lolos seluruh pemeriksaan;body.title.trim()pada baris 68 aman karena validasi sudah memastikan bertipe string. - Baris 85-99 (skenario uji):
POST #1gagal dua aturan sekaligus dan keduanya muncul didetail— bukan hanya yang pertama.POST #3membuktikan normalisasi: judul dikirim dengan spasi berlebih, tersimpan rapi setelah dipangkas.
Untuk API yang lebih besar, aturan validasi seperti ini biasanya dinyatakan sebagai schema dan dijalankan library seperti Zod, Ajv, atau Joi. Prinsip kerjanya sama persis dengan fungsi validasiTugas kita — data masuk, daftar galat keluar — sehingga pemahaman manual ini tetap menjadi bekal saat memakai library tersebut. Sementara itu, ada satu masalah yang belum kita sentuh: GET /tugas selalu mengembalikan seluruh koleksi. Chapter Pagination dan Filtering membatasi jumlah data dengan query parameter.