Routing yang kita bangun pada Bagian 1 baru menyalurkan permintaan; sekarang kita isi dengan logika bisnis. Empat operasi CRUD — Create, Read, Update, Delete — kita wujudkan sebagai handler pada src/tugas.js, lengkap dengan validasi input dan status code yang disiplin. Ada satu keputusan desain yang memandu seluruh bagian ini: handler tidak boleh tahu di mana data disimpan. Handler hanya berbicara pada kontrak penyimpanan, sehingga Bagian 3 nanti bisa menukar penyimpanan memori dengan berkas JSON tanpa menyentuh satu baris handler pun.

Kontrak Penyimpanan Tugas #

Handler membutuhkan lima kemampuan dari penyimpanan, dan kita tetapkan sebagai kontrak berupa method:

MethodTugasHasil
daftar()Ambil seluruh tugasArray tugas
cari(id)Ambil satu tugasTugas atau undefined
tambah(data)Simpan tugas baruTugas lengkap dengan id
ubah(id, data)Gabungkan perubahan pada satu tugasTugas atau undefined
hapus(id)Buang satu tugasTugas yang terbuang atau undefined

Seluruh method dideklarasikan async sejak awal. Penyimpanan memori memang tidak butuh await, tetapi kontrak yang sama akan dipakai penyimpanan berkas JSON pada Bagian 3 — dan operasi berkas selalu asynchronous. Berikut implementasi pertamanya:

js src/tugas.js
export function buatTugasMemori(daftarAwal = []) {
  let daftarTugas = daftarAwal.map((t) => ({ ...t }));
  let idBerikutnya = daftarTugas.reduce((maks, t) => Math.max(maks, t.id), 0) + 1;

  return {
    async daftar() {
      return daftarTugas.map((t) => ({ ...t }));
    },
    async cari(id) {
      const tugas = daftarTugas.find((t) => t.id === id);
      return tugas === undefined ? undefined : { ...tugas };
    },
    async tambah(data) {
      const tugas = { id: idBerikutnya++, judul: data.judul, status: data.status };
      daftarTugas.push(tugas);
      return { ...tugas };
    },
    async ubah(id, data) {
      const indeks = daftarTugas.findIndex((t) => t.id === id);
      if (indeks === -1) return undefined;
      daftarTugas[indeks] = { ...daftarTugas[indeks], ...data, id };
      return { ...daftarTugas[indeks] };
    },
    async hapus(id) {
      const indeks = daftarTugas.findIndex((t) => t.id === id);
      if (indeks === -1) return undefined;
      return { ...daftarTugas.splice(indeks, 1)[0] };
    },
  };
}
  1. Baris 36-37 (data awal dan id berikutnya): idBerikutnya dihitung dari id terbesar data awal ditambah satu, bukan dari panjang array. Setelah tugas terakhir dihapus, panjang menyusut — penghitung seperti itulah yang membuat id lama terpakai ulang untuk resource berbeda.
  2. Baris 39-46 (daftar dan cari): keduanya mengembalikan salinan lewat spread { ...t } agar pemanggil tidak bisa mengubah data internal penyimpanan tanpa sengaja.
  3. Baris 52-56 (ubah): penggabungan { ...lama, ...data, id } membuat handler cukup mengirim field yang berubah; id ditulis terakhir agar selalu tetap.

Validasi Sebelum Penyimpanan #

Data dari client tidak pernah dipercaya tanpa diperiksa. Kita memakai pemisahan dua rasa kesalahan dari Section REST API: body yang tidak bisa diurai menjawab 400, body yang terbaca tetapi melanggar aturan menjawab 422 — pembagian yang mengikuti keluarga status code pada HTTP. Seluruh aturan isi berkumpul dalam satu fungsi murni yang mengembalikan daftar galat, tidak pernah melempar error:

js src/tugas.js
export function validasiTugas(input, opsi = {}) {
  const sebagian = opsi.sebagian ?? false;
  const galat = [];

  if (typeof input !== 'object' || input === null || Array.isArray(input)) {
    return [{ field: 'body', pesan: 'body harus berupa object JSON' }];
  }

  const { judul, status } = input;
  if (judul === undefined) {
    if (!sebagian) galat.push({ field: 'judul', pesan: 'judul wajib diisi' });
  } else if (typeof judul !== 'string') {
    galat.push({ field: 'judul', pesan: 'judul harus berupa string' });
  } else if (judul.trim().length < 3 || judul.trim().length > 80) {
    galat.push({ field: 'judul', pesan: 'judul harus 3-80 karakter setelah dipangkas' });
  }

  if (status !== undefined && !STATUS_VALID.includes(status)) {
    galat.push({ field: 'status', pesan: 'status harus salah satu dari: todo, doing, done' });
  }

  if (sebagian && judul === undefined && status === undefined) {
    galat.push({ field: 'body', pesan: 'kirim minimal salah satu field: judul atau status' });
  }

  return galat;
}
  1. Baris 11-13 (bentuk body): body berupa string atau array JSON langsung ditolak dengan satu galat — handler tetap aman karena destructuring hanya berjalan untuk object.
  2. Baris 16-22 (judul): field judul wajib untuk pembuatan, harus berupa string, dan panjangnya 3-80 karakter setelah trim — sehingga spasi pengisi tidak lolos.
  3. Baris 24-26 (status): status opsional, tetapi bila diisi harus salah satu dari todo, doing, atau done.
  4. Baris 28-30 (mode sebagian): untuk PATCH, keduanya boleh tidak ada — asalkan minimal satu field dikirim; kalau tidak, satu galat menjelaskan permintaannya.

Handler Endpoint #

Dengan validasi dan kontrak penyimpanan di tangan, handler tinggal merangkai keduanya. Dua helper kecil menjaga jawaban galat tetap seragam:

js src/tugas.js
async function bacaBody(req, res) {
  try {
    return await bacaBodyJson(req);
  } catch {
    kirimError(res, 400, 'JSON_TIDAK_VALID', 'Body permintaan bukan JSON valid');
    return undefined;
  }
}

function kirimValidasi(res, galat) {
  kirimError(res, 422, 'VALIDASI_GAGAL', 'Data tugas tidak lolos validasi', galat);
}

export async function tanganiDaftar({ res, url, penyimpanan }) {
  const status = url.searchParams.get('status');
  const semua = await penyimpanan.daftar();
  kirimJson(res, 200, status === null ? semua : semua.filter((t) => t.status === status));
}

export async function tanganiBuat({ req, res, penyimpanan }) {
  const body = await bacaBody(req, res);
  if (body === undefined) return;

  const galat = validasiTugas(body);
  if (galat.length > 0) return kirimValidasi(res, galat);

  const tugas = await penyimpanan.tambah({
    judul: body.judul.trim(),
    status: body.status ?? 'todo',
  });
  res.writeHead(201, { 'Content-Type': 'application/json', Location: `/tugas/${tugas.id}` });
  res.end(JSON.stringify(tugas));
}
  1. Baris 66-73 (bacaBody): pembungkus bacaBodyJson yang menangkap SyntaxError, menjawab 400, lalu mengembalikan undefined sebagai tanda bahwa response sudah terkirim dan handler harus berhenti.
  2. Baris 79-83 (tanganiDaftar): query ?status= dibaca dari searchParams; nilai null berarti parameter tidak ada sehingga seluruh koleksi dikembalikan tanpa disaring.
  3. Baris 85-98 (tanganiBuat): urutannya ketat — baca body, validasi, simpan, jawab 201. Header Location menunjuk alamat resource baru sehingga client tahu harus membaca ke mana.

Handler untuk operasi item mengikuti pola yang sama dengan satu percabangan penting:

js src/tugas.js
export async function tanganiUbah({ req, res, penyimpanan, cocok }) {
  const id = Number(cocok[1]);
  if ((await penyimpanan.cari(id)) === undefined) {
    return kirimError(res, 404, 'TIDAK_ADA', `Tugas ${id} tidak ditemukan`);
  }

  const body = await bacaBody(req, res);
  if (body === undefined) return;

  const sebagian = req.method === 'PATCH';
  const galat = validasiTugas(body, { sebagian });
  if (galat.length > 0) return kirimValidasi(res, galat);

  const perubahan = {};
  if (body.judul !== undefined) perubahan.judul = body.judul.trim();
  if (body.status !== undefined) perubahan.status = body.status;
  if (!sebagian && body.status === undefined) perubahan.status = 'todo';

  kirimJson(res, 200, await penyimpanan.ubah(id, perubahan));
}

export async function tanganiHapus({ res, penyimpanan, cocok }) {
  const id = Number(cocok[1]);
  const terhapus = await penyimpanan.hapus(id);
  if (terhapus === undefined) {
    return kirimError(res, 404, 'TIDAK_ADA', `Tugas ${id} tidak ditemukan`);
  }
  kirimJson(res, 204, null);
}
  1. Baris 111-113 (cek keberadaan lebih dulu): PUT dan PATCH pada id yang tidak ada menjawab 404 sebelum body dibaca — sama seperti perilaku pada Section REST API.
  2. Baris 118-125 (PUT vs PATCH): PATCH hanya menggabungkan field yang dikirim, sedangkan PUT mengganti seluruh isi — bila status tidak dikirim, nilainya kembali ke todo karena resource penggantinya memang belum punya status lain.
  3. Baris 130-137 (tanganiHapus): penghapusan menjawab 204 tanpa body lewat kirimJson(res, 204, null).

Sesi Lengkap API #

Sesi demo berikut menjalankan siklus utuh pada penyimpanan memori — membaca koleksi awal, membuat tugas, menyaring, membaca detail, mengubah lewat PATCH dan PUT, menghapus, lalu menyentuh empat jalur galat:

bash
node src/demo.js

Output yang Dihasilkan:

text
GET /tugas -> 200 [{"id":1,"judul":"Menulis dokumentasi API","status":"doing"},{"id":2,"judul":"Menyiapkan rapat mingguan","status":"todo"}]
POST /tugas -> 201 (lokasi: /tugas/3) {"id":3,"judul":"Meninjau pull request","status":"todo"}
GET /tugas?status=todo -> 200 [{"id":2,"judul":"Menyiapkan rapat mingguan","status":"todo"},{"id":3,"judul":"Meninjau pull request","status":"todo"}]
GET /tugas/1 -> 200 {"id":1,"judul":"Menulis dokumentasi API","status":"doing"}
PATCH /tugas/3 -> 200 {"id":3,"judul":"Meninjau pull request","status":"doing"}
PUT /tugas/1 -> 200 {"id":1,"judul":"Menulis dokumentasi API v2","status":"todo"}
DELETE /tugas/2 -> 204 (body kosong)
GET /tugas/2 -> 404 {"error":{"kode":"TIDAK_ADA","pesan":"Tugas 2 tidak ditemukan"}}
POST /tugas (judul terlalu pendek) -> 422 {"error":{"kode":"VALIDASI_GAGAL","pesan":"Data tugas tidak lolos validasi","detail":[{"field":"judul","pesan":"judul harus 3-80 karakter setelah dipangkas"}]}}
POST /tugas (body bukan JSON) -> 400 {"error":{"kode":"JSON_TIDAK_VALID","pesan":"Body permintaan bukan JSON valid"}}
PATCH /tugas/99 -> 404 {"error":{"kode":"TIDAK_ADA","pesan":"Tugas 99 tidak ditemukan"}}
DELETE /tugas -> 405 {"error":{"kode":"METODE_TIDAK_DIIZINKAN","pesan":"Metode DELETE tidak didukung pada endpoint ini"}}

Perhatikan tiga baris kunci pada output: POST mengembalikan 201 dengan lokasi /tugas/3, PUT /tugas/1 mengganti judul sekaligus status, dan DELETE /tugas/2 diikuti GET /tugas/2 yang menjawab 404 — bukti bahwa state server benar-benar berubah, bukan sekadar status code yang berubah.

Handler kini lengkap dan berdisiplin status code, tetapi semua data masih hidup di memori: begitu proses berhenti, seluruh tugas ikut pergi. Bagian 3 memindahkan penyimpanan ke berkas JSON dengan penulisan atomik, tanpa mengubah satu baris handler yang baru saja kita tulis.