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:
// 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:
- Baris 3-10 (
kirimJson): helper punya dua mode. Nilaidataberupa object dikirim sebagai JSON dengan headerContent-Type; nilainullmengirim response tanpa body — mode yang dipakaiDELETEuntuk status 204. - Baris 12-16 (
kirimError): bentuk error{ error: { kode, pesan } }didefinisikan sekali di satu tempat, sama seperti kesepakatan pada Section REST API. Parameterdetailbersifat opsional; nanti di Bagian 2 kita memakainya untuk daftar galat validasi. - Baris 18-22 (
bacaBodyJson): object request adalah async iterable, jadi body dikumpulkan potongan demi potongan lewatfor await, digabung denganBuffer.concat, lalu diuraiJSON.parse. Bila body bukan JSON valid,JSON.parsemelemparSyntaxError— 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:
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:
- Baris 17-22 (enam rute): dua pola path mencukupi untuk seluruh API — koleksi
/tugasdan item/tugas/{id}. Regular expression^\/tugas\/(\d+)$menangkap id sebagaicocok[1]; penanda^dan$memastikan/tugas/1/komentaratau/tugas/tidak cocok secara tidak sengaja. - Baris 25-34 (
cocokRute): fungsi ini memisahkan dua kondisi berbeda. Bila path dikenal tetapi method tidak cocok,pathDikenaltetaptrue— 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:
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');
}
});
}- 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. - Baris 41 (parsing URL):
new URL(req.url, ...)memisahkanpathnamedari query string sekaligus menyiapkansearchParams— cara baca?status=todoyang akan dipakai Bagian 2. - Baris 52 (dispatch): handler menerima satu object konteks berisi
req,res,url,penyimpanan, dancocok. Satu parameter bernama menjaga signature tetap rapi meski konteks bertambah. - Baris 53-57 (error terpusat): kesalahan tak terduga dicatat ke log server, lalu dijawab 500 tanpa membocorkan detail internal. Pemeriksaan
res.headersSentmenghindari 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:
node src/demo-routing.jsOutput yang Dihasilkan:
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.