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:
| Method | Tugas | Hasil |
|---|---|---|
daftar() | Ambil seluruh tugas | Array tugas |
cari(id) | Ambil satu tugas | Tugas atau undefined |
tambah(data) | Simpan tugas baru | Tugas lengkap dengan id |
ubah(id, data) | Gabungkan perubahan pada satu tugas | Tugas atau undefined |
hapus(id) | Buang satu tugas | Tugas 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:
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] };
},
};
}- Baris 36-37 (data awal dan id berikutnya):
idBerikutnyadihitung 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. - Baris 39-46 (
daftardancari): keduanya mengembalikan salinan lewat spread{ ...t }agar pemanggil tidak bisa mengubah data internal penyimpanan tanpa sengaja. - Baris 52-56 (
ubah): penggabungan{ ...lama, ...data, id }membuat handler cukup mengirim field yang berubah;idditulis 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:
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;
}- 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.
- Baris 16-22 (judul): field
judulwajib untuk pembuatan, harus berupa string, dan panjangnya 3-80 karakter setelahtrim— sehingga spasi pengisi tidak lolos. - Baris 24-26 (status):
statusopsional, tetapi bila diisi harus salah satu daritodo,doing, ataudone. - 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:
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));
}- Baris 66-73 (
bacaBody): pembungkusbacaBodyJsonyang menangkapSyntaxError, menjawab 400, lalu mengembalikanundefinedsebagai tanda bahwa response sudah terkirim dan handler harus berhenti. - Baris 79-83 (
tanganiDaftar): query?status=dibaca darisearchParams; nilainullberarti parameter tidak ada sehingga seluruh koleksi dikembalikan tanpa disaring. - Baris 85-98 (
tanganiBuat): urutannya ketat — baca body, validasi, simpan, jawab 201. HeaderLocationmenunjuk alamat resource baru sehingga client tahu harus membaca ke mana.
Handler untuk operasi item mengikuti pola yang sama dengan satu percabangan penting:
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);
}- Baris 111-113 (cek keberadaan lebih dulu):
PUTdanPATCHpada id yang tidak ada menjawab 404 sebelum body dibaca — sama seperti perilaku pada Section REST API. - Baris 118-125 (
PUTvsPATCH):PATCHhanya menggabungkan field yang dikirim, sedangkanPUTmengganti seluruh isi — bilastatustidak dikirim, nilainya kembali ketodokarena resource penggantinya memang belum punya status lain. - Baris 130-137 (
tanganiHapus): penghapusan menjawab 204 tanpa body lewatkirimJson(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:
node src/demo.jsOutput yang Dihasilkan:
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.