Arsitektur & Best Practices — Node.js

Mengorganisir kode Express.js agar maintainable saat project berkembang. Struktur folder yang baik: src/ ├── routes/ ← Route definitions │ ├── users.js │ └──…

Arsitektur adalah cara membagi kode ke beberapa file dan lapisan, supaya tiap bagian punya satu tugas. Bayangin warung makan yang tumbuh. Awalnya satu orang mengerjakan semua: menerima pesanan, memasak, mengambil bahan di gudang, dan mengantar piring. Begitu ramai, tugasnya dibagi: kasir menerima pesanan dan meneruskannya, pelayan mencatat pesanan dan mengantar piring, koki memasak sesuai resep, penjaga gudang mengambil bahan. Koki gak perlu tahu nomor meja, pelayan gak perlu tahu letak rak di gudang. Di Express, keempatnya adalah route, controller, service, dan repository.

🧪 Tebakan cepat: catatan-api sudah dipecah jadi empat lapisan. Untuk testing, kamu mau datanya diambil dari array di memori, bukan dari file catatan.db. Berapa file yang harus diubah? Tebak dulu, jawabannya ada di bagian "Ganti penyimpanan, satu file berubah".

Sebelum: semua di satu handler

Di pelajaran database, route catatan-api mengerjakan semuanya sendiri:

app.get("/catatan/:id", (req, res) => {
  const row = db.prepare("SELECT * FROM catatan WHERE id = ?").get(Number(req.params.id));
  if (!row) return res.status(404).json({ error: "Catatan tidak ditemukan" });
  res.json(row);
});

Satu handler tahu URL-nya, membaca req, menulis SQL, memilih status, dan mengirim JSON. Dengan dua route masih enak dibaca. Dengan dua puluh route, SQL yang sama tersalin di banyak tempat, dan aturan bisnis tersebar di mana-mana.

Empat lapisan dan apa yang boleh mereka tahu

Lapisan File Tugasnya Boleh tahu Gak boleh tahu
Route routes/catatan.js URL dan method ke function controller Express Router data, SQL
Controller controllers/catatanController.js baca req, panggil service, pilih status dan JSON req, res, status code SQL, tabel
Service services/catatanService.js aturan bisnis data catatan, aturannya req, res, status code, SQL
Repository repositories/catatanRepository.js ambil dan simpan data SQL, db HTTP, aturan bisnis

Tiap lapisan cuma memanggil lapisan di bawahnya: route ke controller, controller ke service, service ke repository.

Refactor-nya, dari bawah ke atas

// repositories/catatanRepository.js: cuma urusan data
const { DatabaseSync } = require("node:sqlite");
const db = new DatabaseSync("catatan.db");

async function semua() {
  return db.prepare("SELECT * FROM catatan").all();
}
async function cariById(id) {
  return db.prepare("SELECT * FROM catatan WHERE id = ?").get(id) ?? null;
}
module.exports = { semua, cariById };
// services/catatanService.js: aturan bisnis, tanpa req dan res
const repo = require("../repositories/catatanRepository");

async function daftar() {
  const semua = await repo.semua();
  return [...semua].sort((a, b) => a.judul.localeCompare(b.judul)); // aturan: urut judul
}
async function ambil(id) {
  return repo.cariById(id); // catatan, atau null kalau gak ada
}
module.exports = { daftar, ambil };
// controllers/catatanController.js: HTTP masuk, HTTP keluar
const service = require("../services/catatanService");

async function daftar(req, res) {
  res.json(await service.daftar());
}
async function detail(req, res) {
  const catatan = await service.ambil(Number(req.params.id));
  if (!catatan) return res.status(404).json({ error: "Catatan tidak ditemukan" });
  res.json(catatan);
}
module.exports = { daftar, detail };
// routes/catatan.js: cuma peta URL
const express = require("express");
const controller = require("../controllers/catatanController");

const router = express.Router();
router.get("/", controller.daftar);
router.get("/:id", controller.detail);
module.exports = router;
// app.js: merakit semuanya
const express = require("express");
const catatanRouter = require("./routes/catatan");

const app = express();
app.use(express.json());
app.use("/api/catatan", catatanRouter);
app.listen(3000);

express.Router dan app.use: alamat lengkapnya

express.Router() membuat kumpulan route yang bisa dipasang ke aplikasi utama. app.use("/api/catatan", catatanRouter) menempelkan awalan /api/catatan ke semua route di dalamnya: router.get("/") melayani GET /api/catatan, dan router.get("/:id") melayani GET /api/catatan/2. Alamat lengkap = awalan di app.use + path di router. Router yang dipasang di /api/siswa dengan router.get("/") melayani /api/siswa.

Ganti penyimpanan, satu file berubah

Untuk testing, ganti isi repositories/catatanRepository.js saja:

const catatan = [
  { id: 1, judul: "Belanja", isi: "beli beras 5 kg" },
  { id: 2, judul: "Belajar", isi: "bab Node.js" },
];
async function semua() {
  return catatan;
}
async function cariById(id) {
  return catatan.find((c) => c.id === id) ?? null;
}
module.exports = { semua, cariById };

Nama function dan bentuk hasilnya sama, jadi service, controller, dan route gak berubah sama sekali. Jawabannya satu file. Itu jawaban tebakannya. Tanpa lapisan, kamu harus mencari dan mengganti setiap db.prepare di semua handler.

MVC dan pola ini

MVC (Model-View-Controller) adalah pola lama yang memisahkan data (model), tampilan (view), dan penghubung keduanya (controller). Di API, "view"-nya adalah JSON yang dikirim res.json. Route, controller, service, repository adalah turunan MVC: model dipecah jadi service (aturan) dan repository (data).

Di latihan: userService dan userController

Kode awal punya users (Budi, Siti, Andi), userService.getAll(), dan userController.list(). Lengkapi userService.getById(id) dengan users.find((u) => u.id === id) || null: service cuma mencari dan mengembalikan, gak mencetak apa pun. Di userController.detail(id), cetak "GET /users/" + id + " -> " + JSON.stringify(user) kalau ketemu, atau "GET /users/" + id + " -> 404 Not Found" kalau gak.

⚠️ Jebakan yang sering ketemu

TL;DR: Pecah aplikasi Express jadi empat lapisan yang masing-masing punya satu tugas: route (peta URL, express.Router()), controller (baca req, pilih status, kirim JSON), service (aturan bisnis, tanpa req/res), repository (SQL dan data). Tiap lapisan cuma memanggil lapisan di bawahnya, jadi mengganti penyimpanan cukup mengubah repository. app.use("/api/catatan", router) memasang router, dan alamat lengkapnya awalan itu ditambah path di router. Pola ini turunan MVC (Model-View-Controller). Service mengembalikan data atau null; controller yang memutuskan 404.

Yang akan kamu pelajari