Penyimpanan memori pada Bagian 2 berumur sama dengan proses: begitu server berhenti, seluruh tugas pergi tanpa jejak. API yang sesungguhnya perlu mengingat datanya melewati hidup-mati proses. Pada bagian ini kita mengganti implementasi penyimpanan dengan berkas JSON di data/tugas.json — dan karena handler hanya berbicara pada kontrak lima method dari Bagian 2, tidak ada satu baris handler yang berubah. Yang perlu kita kerjakan hanyalah tiga hal di dalam src/penyimpanan.js: memuat berkas, menghasilkan id, dan menulis ulang berkas dengan aman.
Memuat Berkas dengan Malas #
Berkas data dibaca pada saat pertama kali dibutuhkan, bukan saat modul diimpor. Hasil bacaannya disimpan sebagai cache di memori sehingga permintaan berikutnya tidak membuka berkas lagi:
export function buatPenyimpananJson(berkasData = 'data/tugas.json') {
let daftarTugas = null;
let idBerikutnya = 1;
async function muat() {
if (daftarTugas !== null) return;
try {
daftarTugas = JSON.parse(await readFile(berkasData, 'utf8'));
} catch (err) {
if (err.code !== 'ENOENT') throw err;
daftarTugas = [];
}
idBerikutnya = daftarTugas.reduce((maks, t) => Math.max(maks, t.id), 0) + 1;
}
async function simpan() {
await mkdir(dirname(berkasData), { recursive: true });
const berkasSementara = `${berkasData}.tmp`;
await writeFile(berkasSementara, JSON.stringify(daftarTugas, null, 2));
await rename(berkasSementara, berkasData);- Baris 7-8 (cache):
daftarTugasdimulai darinullsebagai penanda “belum dimuat” — berbeda dari[]yang berarti “sudah dimuat dan memang kosong”. - Baris 10-19 (
muat): baca berkas denganreadFilelaluJSON.parse. ErrorENOENTberarti berkas belum pernah dibuat, jadi koleksi dianggap kosong; error lain — misalnya izin ditolak — diteruskan agar penanganan error terpusat pada server menjawabnya. Setelah muat,idBerikutnyadihitung dari id terbesar yang ada. - Baris 21-25 (
simpan): seluruh koleksi ditulis ulang setiap kali data berubah. Direktoridata/dibuatmkdirdenganrecursivebila belum ada, sehingga server bisa menyala di folder yang masih kosong.
Pembacaan yang malas punya efek samping yang menyenangkan: operasi baca seperti GET /tugas pada server baru tidak pernah membuat berkas — berkas lahir hanya saat penulisan pertama, jadi folder proyek tetap bersih selama tidak ada data.
Id Deterministik dari Id Terbesar #
Aturan id pada penyimpanan berkas sama dengan versi memori: id baru selalu id terbesar yang pernah ada ditambah satu. Implementasinya terlihat pada method tambah:
return {
async daftar() {
await muat();
return daftarTugas.map((t) => ({ ...t }));
},
async cari(id) {
await muat();
const tugas = daftarTugas.find((t) => t.id === id);
return tugas === undefined ? undefined : { ...tugas };
},
async tambah(data) {
await muat();
const tugas = { id: idBerikutnya++, judul: data.judul, status: data.status };
daftarTugas.push(tugas);
await simpan();
return { ...tugas };
},Method ubah dan hapus mengikuti pola yang sama: await muat() lebih dulu, ubah daftarTugas di memori, lalu await simpan() untuk menuliskan seluruh koleksi. Karena id dihitung dari isi berkas — bukan dari jumlah baris — berkas berisi id [1, 3] menghasilkan id berikutnya 4, bukan 3. Id yang sudah dipakai tidak pernah terpakai ulang, dan hasilnya selalu bisa diprediksi dari isi berkas; itulah alasan test pada Bagian 4 bisa mengklaim id pasti tanpa jam acak.
Penulisan Atomik lewat Berkas Sementara #
Menulis langsung ke data/tugas.json memang mudah, tetapi writeFile
memotong berkas lebih dulu lalu menulis bertahap; bila proses berhenti di tengah jalan, berkas tertinggal setengah terisi dan data lama ikut rusak. Karena itu simpan menulis ke berkas sementara terlebih dahulu, lalu memindahkannya:
writeFile('data/tugas.json.tmp', ...)— berkas lama belum disentuh.renamedaridata/tugas.json.tmpkedata/tugas.json—renamepada satu sistem berkas bersifat atomik: berkas tujuan berganti isi dalam satu langkah, tanpa keadaan setengah-jadi.
Bila proses berhenti sebelum rename, yang tertinggal hanyalah berkas .tmp yang bisa dibuang, sementara tugas.json asli tetap utuh. Pola tulis-ke-sementara-lalu-ganti-nama ini adalah teknik standar penulisan berkas konfigurasi dan data — kita sudah membahasnya pada Section Filesystem, dan sekarang memakainya sungguhan.
Data yang Bertahan Melewati Restart #
Demo persistensi memperlihatkan klaim utama bagian ini. Skenarionya: nyalakan server, buat dua tugas, ubah satu lewat PATCH, matikan server, lalu nyalakan server kedua pada proses baru — dan baca koleksinya kembali:
node src/demo-simpan.jsOutput yang Dihasilkan:
POST /tugas -> 201 {"id":1,"judul":"Belajar persistensi JSON","status":"todo"}
POST /tugas -> 201 {"id":2,"judul":"Menutup server dengan rapi","status":"done"}
PATCH /tugas/1 -> 200 {"id":1,"judul":"Belajar persistensi JSON","status":"doing"}
--- isi data/tugas.json ---
[
{
"id": 1,
"judul": "Belajar persistensi JSON",
"status": "doing"
},
{
"id": 2,
"judul": "Menutup server dengan rapi",
"status": "done"
}
]
--- setelah server dinyalakan ulang ---
GET /tugas -> 200 [{"id":1,"judul":"Belajar persistensi JSON","status":"doing"},{"id":2,"judul":"Menutup server dengan rapi","status":"done"}]Tiga bagian output membuktikan tiga hal: dua POST mengembalikan 201 dengan id berurutan, isi data/tugas.json memuat kedua tugas beserta hasil PATCH dalam format yang rapi, dan GET /tugas pada server kedua menemukan data yang sama — perubahan status doing ikut bertahan. Data kini hidup lebih lama dari proses.
Satu batasan perlu kita akui: penyimpanan ini menulis seluruh koleksi pada setiap perubahan dan tidak mengunci berkas, jadi dua permintaan yang bersamaan bisa saling menimpa tulisannya. Untuk API kecil satu proses seperti proyek ini, batasan itu sengaja kita pilih demi kesederhanaan; ketika beban tumbuh, langkah berikutnya adalah database sungguhan seperti node:sqlite. Sebelum ke sana, kita kunci dulu perilaku API ini dengan test otomatis pada Bagian 4.