Setiap API dimulai dari satu kemampuan dasar: menerima permintaan HTTP, mengenali kemana permintaan itu harus diarahkan, lalu mengirim response dengan format yang konsisten. Pada bagian ini kita membangun fondasi tersebut untuk proyek manajemen tugas memakai node:http mentah — tanpa framework. Kita sudah mengenal pola createServer dan listen(0) pada Section HTTP dan Web Servers; sekarang pola itu kita susun menjadi modul yang rapi: helper JSON yang dipakai ulang di semua endpoint, tabel rute yang mudah dibaca, dan pabrik server yang bisa dinyalakan kapan pun kita mau.

Helper JSON yang Dipakai Ulang #

Tiga pekerjaan muncul berulang di hampir semua endpoint: mengirim response JSON, mengirim error dengan bentuk seragam, dan membaca body permintaan. Menulis ketiganya berulang-ulang di dalam setiap handler hanya mengundang ketidakseragaman, jadi kita kumpulkan dalam satu berkas src/json.js:

js src/json.js
// Helper JSON untuk membaca body permintaan dan menulis response HTTP.

export 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));
}

export function kirimError(res, kode, kodeError, pesan, detail) {
  const error = { kode: kodeError, pesan };
  if (detail) error.detail = detail;
  kirimJson(res, kode, { error });
}

export async function bacaBodyJson(req) {
  const potongan = [];
  for await (const data of req) potongan.push(data);
  return JSON.parse(Buffer.concat(potongan).toString('utf8'));
}

Mari kita bedah isi berkas ini:

  1. Baris 3-10 (kirimJson): helper 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.
  2. Baris 12-16 (kirimError): bentuk error { error: { kode, pesan } } didefinisikan sekali di satu tempat, sama seperti kesepakatan pada Section REST API. Parameter detail bersifat opsional; nanti di Bagian 2 kita memakainya untuk daftar galat validasi.
  3. Baris 18-22 (bacaBodyJson): object request adalah async iterable, jadi body dikumpulkan potongan demi potongan lewat for await, digabung dengan Buffer.concat, lalu diurai JSON.parse. Bila body bukan JSON valid, JSON.parse melempar SyntaxError — handler di Bagian 2 yang menangkapnya dan menjawab 400.

Tabel Rute dan Pencocokannya #

Rute bisa ditulis sebagai tangga if yang panjang, tetapi cara itu sulit dibaca begitu endpoint bertambah. Kita memilih mendata rute sebagai data — array RUTE yang memetakan kombinasi method dan pola path ke fungsi handler. Path setiap permintaan diperiksa terhadap pola ini setelah diurai oleh class URL , yang sekaligus menyiapkan query string untuk dibaca:

js src/server.js
const RUTE = [
  { metode: 'GET', pola: /^\/tugas$/, aksi: tanganiDaftar },
  { metode: 'POST', pola: /^\/tugas$/, aksi: tanganiBuat },
  { metode: 'GET', pola: /^\/tugas\/(\d+)$/, aksi: tanganiDetail },
  { metode: 'PUT', pola: /^\/tugas\/(\d+)$/, aksi: tanganiUbah },
  { metode: 'PATCH', pola: /^\/tugas\/(\d+)$/, aksi: tanganiUbah },
  { metode: 'DELETE', pola: /^\/tugas\/(\d+)$/, aksi: tanganiHapus },
];

function cocokRute(metode, pathname) {
  let pathDikenal = false;
  for (const rute of RUTE) {
    const cocok = pathname.match(rute.pola);
    if (!cocok) continue;
    pathDikenal = true;
    if (rute.metode === metode) return { aksi: rute.aksi, cocok };
  }
  return { aksi: null, pathDikenal };
}

Beberapa keputusan penting berada pada potongan di atas:

  1. Baris 17-22 (enam rute): dua pola path mencukupi untuk seluruh API — koleksi /tugas dan item /tugas/{id}. Regular expression ^\/tugas\/(\d+)$ menangkap id sebagai cocok[1]; penanda ^ dan $ memastikan /tugas/1/komentar atau /tugas/ tidak cocok secara tidak sengaja.
  2. Baris 25-34 (cocokRute): fungsi ini memisahkan dua kondisi berbeda. Bila path dikenal tetapi method tidak cocok, pathDikenal tetap true — informasi yang kita pakai untuk menjawab 405, bukan 404. Bila method cocok, fungsi mengembalikan handler beserta hasil tangkapan regex-nya.

Perbedaan 404 dan 405 penting bagi client: menurut daftar status code HTTP , 404 berarti “alamat ini tidak ada”, 405 berarti “alamat ada tetapi method-mu salah”. Keduanya memakai bentuk error seragam sehingga pesannya bisa langsung ditampilkan.

Pabrik Server dalam buatServer #

Fungsi buatServer merangkai semuanya: membuat server lewat http.createServer , memasang routing, dan menyelimuti seluruh handler dengan try/catch:

js src/server.js
export function buatServer(opsi = {}) {
  const penyimpanan = opsi.penyimpanan ?? buatPenyimpananJson(opsi.berkasData);

  return http.createServer(async (req, res) => {
    try {
      const url = new URL(req.url, `http://${HOST}`);
      const { aksi, cocok, pathDikenal } = cocokRute(req.method, url.pathname);

      if (!aksi) {
        if (pathDikenal) {
          return kirimError(res, 405, 'METODE_TIDAK_DIIZINKAN',
            `Metode ${req.method} tidak didukung pada endpoint ini`);
        }
        return kirimError(res, 404, 'ENDPOINT_TIDAK_ADA', 'Endpoint tidak ditemukan');
      }

      await aksi({ req, res, url, penyimpanan, cocok });
    } catch (err) {
      console.error(`[kesalahan internal] ${err.message}`);
      if (res.headersSent) return res.end();
      kirimError(res, 500, 'KESALAHAN_INTERNAL', 'Terjadi kesalahan tak terduga di sisi server');
    }
  });
}
  1. Baris 37 (injeksi penyimpanan): penyimpanan bisa disuntik lewat opsi.penyimpanan; bila tidak, server memakai penyimpanan JSON bawaan. Pola ini membuat test nanti bisa mengganti penyimpanan tanpa menyentuh berkas config.
  2. Baris 41 (parsing URL): new URL(req.url, ...) memisahkan pathname dari query string sekaligus menyiapkan searchParams — cara baca ?status=todo yang akan dipakai Bagian 2.
  3. Baris 52 (dispatch): handler menerima satu object konteks berisi req, res, url, penyimpanan, dan cocok. Satu parameter bernama menjaga signature tetap rapi meski konteks bertambah.
  4. Baris 53-57 (error terpusat): kesalahan tak terduga dicatat ke log server, lalu dijawab 500 tanpa membocorkan detail internal. Pemeriksaan res.headersSent menghindari upaya mengirim response yang sudah setengah terkirim.

Melihat Perilaku Routing #

Sesi demo routing menyalakan server dengan penyimpanan kosong lalu mengirim lima permintaan yang menyentuh setiap cabang keputusan router:

bash
node src/demo-routing.js

Output yang Dihasilkan:

text
GET /tugas -> 200 []
GET /tugas/1 -> 404 {"error":{"kode":"TIDAK_ADA","pesan":"Tugas 1 tidak ditemukan"}}
GET /halo -> 404 {"error":{"kode":"ENDPOINT_TIDAK_ADA","pesan":"Endpoint tidak ditemukan"}}
DELETE /tugas -> 405 {"error":{"kode":"METODE_TIDAK_DIIZINKAN","pesan":"Metode DELETE tidak didukung pada endpoint ini"}}
POST /tugas/1 -> 405 {"error":{"kode":"METODE_TIDAK_DIIZINKAN","pesan":"Metode POST tidak didukung pada endpoint ini"}}

Dua rasa 404 terlihat berbeda: GET /tugas/1 menjawab TIDAK_ADA karena path cocok pola item dan handler-lah yang menemukan koleksi kosong, sedangkan GET /halo menjawab ENDPOINT_TIDAK_ADA karena tidak ada pola yang cocok. Dua baris terakhir memperlihatkan 405: path /tugas dikenali tetapi DELETE dan POST pada /tugas/1 tidak terdaftar di tabel rute untuk kombinasi tersebut.

Routing kini hidup dan setiap permintaan sampai ke handler yang benar — atau dijawab dengan galat yang jelas. Yang belum ada adalah logika bisnisnya: bagaimana tugas divalidasi, disimpan, dan dikembalikan dengan status code yang tepat. Itu pekerjaan Bagian 2.