Aplikasi Node.js biasanya dimulai dari satu berkas: satu handler membaca permintaan, memvalidasi input, menjalankan query, lalu memformat jawaban. Selama aplikasi masih kecil, pola satu-serba-bisa tersebut terasa cepat dan praktis. Begitu jumlah fitur bertambah, kode yang serba campur berubah menjadi titik gesekan: setiap fitur baru menyentuh kode yang sama, dan setiap perubahan aturan bisnis memaksa kita membongkar kode yang seharusnya tidak berhubungan. Pola arsitektur berlapis (layered architecture) menjawab masalah ini dengan membagi kode menjadi lapisan-lapisan yang masing-masing memegang satu tanggung jawab. Pada chapter ini kita mempelajari konsep lapisan, arah ketergantungan yang sehat, dan wujudnya dalam satu demo irisan vertikal yang bisa dijalankan.
Masalah: Satu Fungsi untuk Semua #
Perhatikan bentuk handler yang sering muncul di aplikasi yang tumbuh tanpa arsitektur. Satu fungsi mengerjakan urusan HTTP, urusan bisnis, dan urusan data sekaligus:
app.post('/tugas', async (req, res) => {
// urusan HTTP: membaca body permintaan
const { judul, status } = await bacaBody(req);
// urusan bisnis: validasi aturan tugas
if (!judul || judul.length > 80) {
return res.status(400).json({ galat: 'JUDUL_SALAH' });
}
// urusan data: query penyimpanan tertanam di handler
const hasil = db.prepare('INSERT INTO tugas (judul, status) VALUES (?, ?)')
.run(judul, status ?? 'belum');
// kembali ke urusan HTTP: format jawaban
res.status(201).json({ id: hasil.lastInsertRowid, judul });
});Bentuk ini masih bisa dibaca, tetapi gejalanya menumpuk seiring waktu. Aturan bisnis seperti batas panjang judul akan terduplikasi di setiap rute yang membutuhkannya. Handler tidak bisa diuji tanpa menjalankan server HTTP, karena aturan bisnis menempel pada req dan res. Ketika penyimpanan berpindah dari Map ke SQLite, kita harus menyusuri semua rute satu per satu. Setiap perubahan — sekecil apa pun — menyentuh kode yang sama persis, dan itulah sumber regresi.
Tiga Lapisan dan Arah Ketergantungan #
Arsitektur berlapis memecah kode berdasarkan jenis tanggung jawab, bukan berdasarkan fitur. Tabel berikut merangkum pembagian yang paling umum dipakai aplikasi Node.js:
| Lapisan | Tanggung jawab utama | Tidak boleh menyentuh |
|---|---|---|
| Presentasi (rute, handler, CLI) | Membaca permintaan, memetakan status code, serialisasi jawaban | Query penyimpanan, aturan bisnis |
| Service | Validasi, aturan bisnis, koordinasi antar data | Object req/res, detail query |
| Repository | Query penyimpanan, konversi baris data menjadi object domain | Status code, aturan bisnis |
Kunci pola ini bukan sekadar jumlah lapisan, melainkan arah ketergantungan: lapisan luar boleh memanggil lapisan dalam, tetapi tidak pernah sebaliknya. Handler memanggil service; service memanggil repository; repository tidak pernah tahu bahwa service ada.
Demo: Irisan Vertikal Tiga Lapisan #
Kita wujudkan ketiga lapisan dalam satu berkas yang bisa dijalankan. Agar fokus pada struktur, lapisan presentasi di demo ini memproses object biasa { method, path, body } dan mengembalikan { status, body } — bentuk yang sama dengan HTTP, tetapi tanpa jaringan sungguhan. Server node:http
asli menyusul di chapter Pola Service Layer.
// === Lapisan data: satu-satunya tempat yang tahu cara menyimpan ===
function buatRepoTugas() {
const tugas = new Map();
let idBerikutnya = 1;
return {
simpan(tugasBaru) {
const lengkap = { id: idBerikutnya++, ...tugasBaru };
tugas.set(lengkap.id, lengkap);
return lengkap;
},
semua() { return [...tugas.values()]; },
cari(id) { return tugas.get(id) ?? null; },
};
}
// === Lapisan service: satu-satunya tempat yang tahu aturan bisnis ===
function buatLayananTugas({ repo, batasJudul = 60 }) {
return {
buat(input) {
const judul = typeof input.judul === 'string' ? input.judul.trim() : '';
if (judul === '') return { ok: false, galat: 'JUDUL_WAJIB' };
if (judul.length > batasJudul) return { ok: false, galat: 'JUDUL_PANJANG' };
return { ok: true, data: repo.simpan({ judul, status: 'belum' }) };
},
daftar() { return repo.semua(); },
};
}
// === Lapisan presentasi: satu-satunya tempat yang bicara format HTTP ===
function buatHandler({ layanan }) {
return function prosesPermintaan(permintaan) {
if (permintaan.method === 'POST' && permintaan.path === '/tugas') {
const hasil = layanan.buat(permintaan.body ?? {});
return hasil.ok
? { status: 201, body: hasil.data }
: { status: 400, body: { galat: hasil.galat } };
}
if (permintaan.method === 'GET' && permintaan.path === '/tugas') {
return { status: 200, body: layanan.daftar() };
}
return { status: 404, body: { galat: 'ENDPOINT_TAK_DIKENAL' } };
};
}
// === Susunan aplikasi: lapisan luar membungkus lapisan dalam ===
const layanan = buatLayananTugas({ repo: buatRepoTugas() });
const handler = buatHandler({ layanan });
const skenario = [
{ method: 'POST', path: '/tugas', body: { judul: 'Belajar arsitektur berlapis' } },
{ method: 'POST', path: '/tugas', body: { judul: ' ' } },
{ method: 'POST', path: '/tugas', body: { judul: 'x'.repeat(61) } },
{ method: 'GET', path: '/tugas' },
{ method: 'GET', path: '/pengguna' },
];
for (const permintaan of skenario) {
const jawaban = handler(permintaan);
console.log(`${permintaan.method} ${permintaan.path} -> ${jawaban.status}`, JSON.stringify(jawaban.body));
}
// === Lapisan presentasi lain: laporan teks untuk CLI ===
function buatLaporan({ layanan }) {
return () => layanan.daftar().map((t) => `${t.id}. [${t.status}] ${t.judul}`).join('\n');
}
const laporan = buatLaporan({ layanan });
console.log('--- laporan CLI ---');
console.log(laporan());
// === Service lain, repo sama, handler tak tersentuh ===
const layananKetat = buatLayananTugas({ repo: buatRepoTugas(), batasJudul: 10 });
const handlerKetat = buatHandler({ layanan: layananKetat });
const jawabanKetat = handlerKetat({ method: 'POST', path: '/tugas', body: { judul: 'Judul yang cukup panjang' } });
console.log('service ketat ->', jawabanKetat.status, JSON.stringify(jawabanKetat.body));Output yang Dihasilkan:
POST /tugas -> 201 {"id":1,"judul":"Belajar arsitektur berlapis","status":"belum"}
POST /tugas -> 400 {"galat":"JUDUL_WAJIB"}
POST /tugas -> 400 {"galat":"JUDUL_PANJANG"}
GET /tugas -> 200 [{"id":1,"judul":"Belajar arsitektur berlapis","status":"belum"}]
GET /pengguna -> 404 {"galat":"ENDPOINT_TAK_DIKENAL"}
--- laporan CLI ---
1. [belum] Belajar arsitektur berlapis
service ketat -> 400 {"galat":"JUDUL_PANJANG"}Mari kita telaah lapisan demi lapisan:
- Baris 2-14 (
buatRepoTugas): Lapisan data. Seluruh pengetahuan tentang penyimpanan terkurung di sini —Mapprivat, penghitung id, dan tiga method publik (simpan,semua,cari). Tidak ada satu baris pun di lapisan ini yang tahu aturan bisnis atau format HTTP. - Baris 17-27 (
buatLayananTugas): Lapisan service. Factory ini menerimarepolewat parameter — bukan mengimpor dari luar — besertabatasJudulyang diberi nilai bawaan 60. Methodbuatmenjalankan dua aturan bisnis (judul wajib terisi dan tidak boleh melebihi batas), lalu mengembalikan object hasil berbentuk{ ok, data | galat }supaya pemanggil cukup memeriksa satu properti. - Baris 30-43 (
buatHandler): Lapisan presentasi. Handler menerjemahkan permintaan menjadi panggilan service, lalu menerjemahkan kembali hasil service menjadi status code:201untuk tugas baru,400untuk aturan yang dilanggar,404untuk path yang tidak dikenal. Tidak ada satu pun aturan bisnis di dalam handler — hanya pemetaan format. - Baris 46-47 (susunan aplikasi): Aplikasi dirakit dari dalam ke luar: repository dibuat lebih dulu, disuntikkan ke service, lalu service disuntikkan ke handler. Arah perakitan ini persis mengikuti arah ketergantungan pada diagram.
- Baris 49-60 (skenario): Lima permintaan menjalankan jalur bahagia dan jalur gagal secara berurutan. Baris ketiga memakai
'x'.repeat(61)untuk menghasilkan judul yang melebihi batas 60 karakter tanpa menulis 61 huruf manual. - Baris 63-68 (
buatLaporan): Presentasi kedua — laporan teks bergaya CLI — memakai service yang sama dengan handler HTTP. Perhatikan bahwalayanandi baris 66 adalah object yang persis sama dengan yang dipakai handler; tidak ada kode service yang disalin. - Baris 71-74 (service ketat): Sebuah service baru dibuat dengan aturan berbeda (
batasJudul: 10) di atas repository baru, lalu dibungkus handler yang sama. Kode handler dan kode repo tidak diubah satu baris pun.
Perubahan yang Tidak Menjalar #
Dua bagian akhir output membuktikan klaim utama pola ini. Laporan CLI di baris 62-68 menghasilkan 1. [belum] Belajar arsitektur berlapis dari service yang sama dengan handler HTTP — artinya menambah satu bentuk presentasi baru tidak menyentuh lapisan dalam sama sekali. Sebaliknya, service ketat di baris 70-74 mengubah aturan bisnis (batas judul menjadi 10 karakter) tanpa mengubah satu baris handler; jawaban 400 muncul dari handler yang sama persis.
Pola ini juga yang membuat pengujian ringan. Karena buatLayananTugas menerima repo sebagai parameter, kita bisa menguji seluruh aturan bisnis dengan repository sederhana di memori — tanpa menjalankan server, tanpa jaringan, tanpa database. Materi pengujian lengkap menanti di Section Testing, khususnya chapter Mocking yang menyuntikkan dependensi palsu.
Kapan Lapisan Menjadi Beban #
Setiap pola punya harga, dan lapisan tidak terkecuali. Semua lapisan membayar biaya berupa berkas dan lompatan baca yang lebih banyak, jadi pola ini layak dipakai hanya ketika perubahan memang sering terjadi:
Dengan konsep tiga lapisan di tangan, kita siap membedah tiap lapisan satu per satu. Kumpulan panduan resmi di Node.js Learn memperkaya perspektif tentang menyusun aplikasi setelah pola dasarnya kita kuasai. Lapisan yang paling sering ditulis lebih dulu adalah lapisan data — dan pola penyusunannya punya nama tersendiri: Pola Repository, chapter berikutnya.