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.
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:
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:
- Baris 3-8 (penyimpanan):
Mapmenampung tugas dengan id sebagai key, dan variableidBerikutnyamenjadi 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. - Baris 10-17 (
kirimJson): Helper ini punya dua mode. Nilaidataberupa object dikirim sebagai JSON dengan headerContent-Type; nilainullmengirim response tanpa body — mode yang dipakaiDELETEuntuk status 204. - Baris 19-24 (
bacaBodyJson): Body permintaan dibaca bertahap lewatfor awaitkarena body berupa stream, lalu potongan-potonganBufferdigabung dan diurai denganJSON.parse. Hasilnya object JavaScript yang siap dipakai handler. - 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. - Baris 35-46 (rute koleksi):
POSTmenaikkanidBerikutnya, memakai?? 'belum'agar status boleh dikosongkan client, lalu menjawab 201 dengan headerLocationrelatif dan representasi tugas baru.GETmengembalikan seluruh nilaiMaplewat spread[...tugas.values()]. - 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. - Baris 56-70 (tiga operasi item):
GETmemakaitugas.has()sebelumget()agarundefinedtidak pernah bocor sebagai response.PUTmembangun object baru dari body — client wajib mengirim representasi lengkap karenaPUTberarti mengganti seluruh isi.DELETEmenghapus key lalu menjawab 204 tanpa body. - 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.