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:

JenisContohStatusPesan untuk client
Format rusakBody bukan JSON valid400Kirim ulang body yang benar-benar JSON
Isi tidak lolos aturantitle hanya 2 karakter, status tidak dikenal422Perbaiki 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:

  1. Tipe: title wajib berupa string — angka, boolean, null, maupun field yang tidak ada, semuanya ditolak.
  2. Kerapian: title dipangkas spasi awal-akhirnya (trim) sebelum diperiksa, sehingga " ab " dihitung dua karakter, bukan enam.
  3. Panjang: setelah dipangkas, title harus 3-80 karakter.
  4. Daftar nilai: status bersifat opsional, tetapi bila diisi harus salah satu dari belum atau selesai.

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:

js
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:

text
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:

  1. 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.
  2. Baris 16-20 (kirimError dengan detail): Bentuk error seragam dari Chapter sebelumnya diperluas dengan field detail yang hanya hadir untuk 422. Client yang tidak mengenal detail tetap bisa membaca kode dan pesan.
  3. Baris 32-34 (pemeriksaan bentuk paling awal): JSON yang valid belum tentu object — "42", true, dan null semuanya JSON sah. Pemeriksaan typeof input !== 'object' || input === null menangkap semuanya sebelum kode sempat membaca input.title dan gagal.
  4. Baris 36-42 (tipe, kerapian, panjang): Operator typeof memastikan title berupa string — pemeriksaan ini sekaligus menolak null, karena typeof null bernilai "object", bukan "string". Pemangkasan trim() dilakukan lebih dulu agar batas panjang dihitung dari isi sebenarnya; batas bawah dan atas diperiksa sebagai dua galat terpisah agar pesannya spesifik.
  5. Baris 44-46 (nilai enum): status yang tidak diisi dibiarkan lolos karena field ini opsional; undefined adalah satu-satunya nilai yang dikecualikan, dan nilai default "belum" diterapkan kemudian di handler.
  6. 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.
  7. Baris 85-99 (skenario uji): POST #1 gagal dua aturan sekaligus dan keduanya muncul di detail — bukan hanya yang pertama. POST #3 membuktikan 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.