Blueprint sudah di tangan; sekarang kita mewujudkannya dalam kode yang berjalan. Empat operasi dasar setiap API data adalah CRUD — Create, Read, Update, Delete — yang pada REST terpetakan ke POST, GET, PUT/PATCH, dan DELETE. Pada Chapter ini kita membangun keempat operasi tersebut di atas penyimpanan di memori: sebuah Map yang berumur sama dengan proses Node.js. Penyimpanan ini hilang begitu server berhenti, tetapi justru itu nilainya — kita bisa fokus pada pola handler tanpa perlu menyiapkan database lebih dulu; integrasi database menanti di Section Database Integration.

Menata Data dan Tiga Helper #

Tugas disimpan dalam Map dengan id sebagai key. Tiga helper kecil menjaga handler tetap ringkas: kirimJson untuk mengirim response JSON (atau body kosong untuk 204), bacaBodyJson untuk membaca body permintaan, dan tidakAda untuk bentuk error 404 yang seragam. Membaca body memakai for await karena object request adalah Readable stream — pola yang sudah kita bahas pada Section HTTP dan Web Servers.

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' }],
  [2, { id: 2, title: 'Menulis dokumentasi API', status: 'belum' }],
]);
let idBerikutnya = 3;

function kirimJson(res, kode, data) {
  if (data === null) {
    res.writeHead(kode);
    return res.end();
  }
  res.writeHead(kode, { 'Content-Type': 'application/json' });
  res.end(JSON.stringify(data));
}

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 tidakAda(res, id) {
  return kirimJson(res, 404, {
    error: { kode: 'TIDAK_ADA', pesan: `Tugas ${id} tidak ditemukan` },
  });
}

const server = http.createServer(async (req, res) => {
  const url = new URL(req.url, `http://${host}`);

  let cocok = url.pathname.match(/^\/tugas$/);
  if (cocok && req.method === 'POST') {
    const body = await bacaBodyJson(req);
    const id = idBerikutnya++;
    const baru = { id, title: body.title, 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 (cocok && req.method === 'GET') {
    return kirimJson(res, 200, [...tugas.values()]);
  }

  cocok = url.pathname.match(/^\/tugas\/(\d+)$/);
  if (!cocok) {
    return kirimJson(res, 404, {
      error: { kode: 'TIDAK_ADA', pesan: 'Endpoint tidak ditemukan' },
    });
  }
  const id = Number(cocok[1]);

  if (req.method === 'GET') {
    return tugas.has(id) ? kirimJson(res, 200, tugas.get(id)) : tidakAda(res, id);
  }
  if (req.method === 'PUT') {
    if (!tugas.has(id)) return tidakAda(res, id);
    const body = await bacaBodyJson(req);
    const diganti = { id, title: body.title, status: body.status };
    tugas.set(id, diganti);
    return kirimJson(res, 200, diganti);
  }
  if (req.method === 'DELETE') {
    if (!tugas.has(id)) return tidakAda(res, id);
    tugas.delete(id);
    return kirimJson(res, 204, null);
  }

  return kirimJson(res, 404, {
    error: { kode: 'TIDAK_ADA', pesan: 'Endpoint tidak ditemukan' },
  });
});

server.listen(0, host, async () => {
  const port = server.address().port;
  const base = `http://${host}:${port}`;

  const buat = await fetch(`${base}/tugas`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ title: 'Belajar REST API' }),
  });
  console.log(`POST /tugas -> ${buat.status}`, `(lokasi: ${buat.headers.get('location')})`, JSON.stringify(await buat.json()));

  const daftar = await fetch(`${base}/tugas`);
  console.log(`GET /tugas -> ${daftar.status}`, JSON.stringify(await daftar.json()));

  const ubah = await fetch(`${base}/tugas/3`, {
    method: 'PUT',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ title: 'Belajar REST API', status: 'selesai' }),
  });
  console.log(`PUT /tugas/3 -> ${ubah.status}`, JSON.stringify(await ubah.json()));

  const hapus = await fetch(`${base}/tugas/3`, { method: 'DELETE' });
  console.log(`DELETE /tugas/3 -> ${hapus.status}`, `(body: ${(await hapus.text()) || 'kosong'})`);

  const cek = await fetch(`${base}/tugas/3`);
  console.log(`GET /tugas/3 -> ${cek.status}`, JSON.stringify(await cek.json()));

  server.close();
  server.closeAllConnections();
});

Output yang Dihasilkan:

text
POST /tugas -> 201 (lokasi: /tugas/3) {"id":3,"title":"Belajar REST API","status":"belum"}
GET /tugas -> 200 [{"id":1,"title":"Belajar Node.js","status":"selesai"},{"id":2,"title":"Menulis dokumentasi API","status":"belum"},{"id":3,"title":"Belajar REST API","status":"belum"}]
PUT /tugas/3 -> 200 {"id":3,"title":"Belajar REST API","status":"selesai"}
DELETE /tugas/3 -> 204 (body: kosong)
GET /tugas/3 -> 404 {"error":{"kode":"TIDAK_ADA","pesan":"Tugas 3 tidak ditemukan"}}

Mari kita telaah bagian demi bagian:

  1. Baris 3-8 (penyimpanan): Map menampung tugas dengan id sebagai key, dan variable idBerikutnya menjadi penghitung naik untuk id baru. Skenario uji di penghujung file menjalankan siklus penuh — buat, daftar, ubah, hapus, cek — sehingga output selalu mencerminkan state awal yang sama.
  2. Baris 10-17 (kirimJson): Helper ini punya dua mode. Nilai data berupa object dikirim sebagai JSON dengan header Content-Type; nilai null mengirim response tanpa body — mode yang dipakai DELETE untuk status 204.
  3. Baris 19-24 (bacaBodyJson): Body permintaan dibaca bertahap lewat for await karena body berupa stream, lalu potongan-potongan Buffer digabung dan diurai dengan JSON.parse. Hasilnya object JavaScript yang siap dipakai handler.
  4. Baris 26-30 (tidakAda): Bentuk error 404 didefinisikan sekali saja di satu tempat. Bila nanti bentuk error berubah, kita cukup mengubah fungsi ini — prinsip yang diperluas pada Chapter Status Code dan Penanganan Error.
  5. Baris 35-46 (rute koleksi): POST menaikkan idBerikutnya, memakai ?? 'belum' agar status boleh dikosongkan client, lalu menjawab 201 dengan header Location relatif dan representasi tugas baru. GET mengembalikan seluruh nilai Map lewat spread [...tugas.values()].
  6. Baris 48-54 (rute item): Regular expression menangkap id dari path dan Number() mengubahnya dari string ke angka. Path yang tidak cocok pola apa pun jatuh ke 404 umum.
  7. Baris 56-70 (tiga operasi item): GET memakai tugas.has() sebelum get() agar undefined tidak pernah bocor sebagai response. PUT membangun object baru dari body — client wajib mengirim representasi lengkap karena PUT berarti mengganti seluruh isi. DELETE menghapus key lalu menjawab 204 tanpa body.
  8. Baris 77-102 (skenario uji): Lima permintaan berurutan membentuk siklus hidup resource: dibuat (201 + lokasi), tampil di daftar (200), diganti (200), dihapus (204), lalu diverifikasi benar-benar tiada (404). Baris terakhir output membuktikan state server berubah — bukan sekadar status code yang berubah.

Keempat operasi CRUD kini bekerja, tetapi perhatikan bahwa penanganan error kita masih setengah jalan: body yang bukan JSON akan menjadi JSON.parse yang gagal dan permintaan menggantung tanpa jawaban. Chapter Status Code dan Penanganan Error menuntaskan masalah itu dengan status code yang tepat dan bentuk error yang seragam.