Database adalah tempat data disimpan di luar program, jadi datanya tetap ada walaupun server mati. Bayangin papan tulis dan buku catatan di laci. Array catatan kita itu papan tulis: gampang ditulis, tapi dihapus tiap kali kelas selesai. Database itu buku di laci: besok pagi isinya masih sama. API sungguhan menyimpan datanya di database.
🧪 Tebakan cepat: kamu POST catatan Olahraga, lalu menghentikan server dengan Ctrl+C dan menjalankan
node app.jslagi. Berapa catatan yang dijawabGET /catatankalau datanya di array? Kalau di database? Tebak dulu, jawabannya ada di bagian "Array hilang saat server mati".
Array hilang saat server mati
Array hidup di memori proses Node. Waktu proses berhenti, memorinya dibuang, dan node app.js berikutnya mulai lagi dari isi array yang tertulis di kode.
| Langkah | Data di array | Data di database |
|---|---|---|
| server jalan | 2 catatan | 2 catatan |
| POST Olahraga | 3 catatan | 3 catatan |
Ctrl+C, lalu node app.js |
2 catatan, Olahraga hilang | 3 catatan, Olahraga masih ada |
Array: 2, database: 3. Itu jawaban tebakannya.
SQLite bawaan Node 24: node:sqlite
SQLite adalah database yang disimpan sebagai satu file biasa, tanpa server database terpisah. Node 24 sudah membawanya, jadi gak perlu npm install:
const { DatabaseSync } = require("node:sqlite");
const db = new DatabaseSync("catatan.db");
db.exec(`
CREATE TABLE IF NOT EXISTS catatan (
id INTEGER PRIMARY KEY AUTOINCREMENT,
judul TEXT NOT NULL,
isi TEXT NOT NULL
)
`);
new DatabaseSync("catatan.db")membuka filecatatan.db, atau membuatnya kalau belum ada.CREATE TABLE IF NOT EXISTSmembuat tabel sekali saja. Restart berikutnya melewatinya, dan datanya tetap.INTEGER PRIMARY KEY AUTOINCREMENTmembuat database yang membagikan id: 1, 2, 3, dan id yang pernah dipakai gak dipakai lagi. PenghitungnextIdgak dibutuhkan lagi.
Di Node 22, node:sqlite masih mencetak ExperimentalWarning: SQLite is an experimental feature and might change at any time. Package better-sqlite3 punya cara pakai yang sama (prepare, get, all, run) untuk versi Node yang lebih lama.
Route yang bicara SQL
app.get("/catatan", (req, res) => {
const rows = db.prepare("SELECT * FROM catatan").all();
res.json(rows);
});
app.get("/catatan/:id", (req, res) => {
const row = db.prepare("SELECT * FROM catatan WHERE id = ?").get(req.params.id);
if (!row) return res.status(404).json({ error: "Catatan tidak ditemukan" });
res.json(row);
});
app.post("/catatan", (req, res) => {
const { judul, isi } = req.body;
const info = db.prepare("INSERT INTO catatan (judul, isi) VALUES (?, ?)").run(judul, isi);
const baru = db.prepare("SELECT * FROM catatan WHERE id = ?").get(info.lastInsertRowid);
res.status(201).json(baru);
});
app.delete("/catatan/:id", (req, res) => {
const info = db.prepare("DELETE FROM catatan WHERE id = ?").run(req.params.id);
if (info.changes === 0) return res.status(404).json({ error: "Catatan tidak ditemukan" });
res.status(204).end();
});
db.prepare(sql)menyiapkan satu perintah SQL..all()mengembalikan semua baris sebagai array,.get()satu baris atauundefined,.run()untuk perintah yang mengubah data..run()mengembalikan{ changes, lastInsertRowid }: berapa baris yang berubah, dan id baris terakhir yang dibuat.- Validasi dari pelajaran sebelumnya tetap dipasang di POST. Tanpanya, body tanpa
judulberakhir dengan 500:TypeError: Provided value cannot be bound to SQLite parameter 1.
Placeholder ?: nilainya dikirim terpisah
? adalah tempat kosong di SQL. Nilainya dikirim ke database terpisah dari teks SQL-nya, jadi apa pun isinya, dia dianggap data. Bandingkan dengan menempelkan nilai langsung ke teks SQL:
// BAHAYA: nilai dari URL ditempel ke teks SQL
const info = db.prepare(`DELETE FROM catatan WHERE id = ${req.params.id}`).run();
| Request | SQL yang sampai ke database | Hasil |
|---|---|---|
DELETE /catatan/2 |
DELETE FROM catatan WHERE id = 2 |
catatan 2 terhapus |
DELETE /catatan/0%20OR%201=1 |
DELETE FROM catatan WHERE id = 0 OR 1=1 |
semua catatan terhapus, 204 |
sama, dengan ? |
DELETE FROM catatan WHERE id = ? + nilai "0 OR 1=1" |
gak ada yang cocok, 404 |
Ini namanya SQL injection: input pengguna berubah jadi bagian dari perintah SQL. 1=1 selalu benar, jadi WHERE cocok untuk semua baris. Input yang jujur pun bisa merusak: judul Jum'at membuat VALUES ('Jum'at', ...), dan SQLite menolaknya dengan near "at": syntax error, lalu Express menjawab 500. Dengan ?, Jum'at tersimpan apa adanya.
MySQL dan PostgreSQL: driver dan pool
Untuk database server seperti MySQL, Node butuh driver dari npm. Dengan mysql2:
const mysql = require("mysql2/promise");
const pool = mysql.createPool({ host: "localhost", user: "root", database: "sekolah" });
const [rows] = await pool.execute("SELECT * FROM siswa WHERE id = ?", [id]);
createPoolmembuat connection pool: beberapa koneksi yang dibuka sekali lalu dipakai bergantian, jadi tiap request gak perlu menyambung ulang.pool.execute(sql, [nilai])memakai?yang sama, nilainya dalam array.- Hasilnya Promise, jadi pakai
await.const [rows] = ...mengambil isi pertama dari array hasilnya, yaitu baris-barisnya.
PostgreSQL dengan package pg memakai $1, $2 sebagai pengganti ?.
ORM dan query builder
Banyak proyek gak menulis SQL sendiri. ORM (Object Relational Mapper) seperti Prisma dan Sequelize membuat tabel terasa seperti object JavaScript: prisma.catatan.findMany(). Query builder seperti Knex menyusun SQL lewat function: knex("catatan").where({ id }). Ketiganya tetap memakai placeholder di belakang layar.
Di latihan: database tiruan dari array
Latihannya memakai array products sebagai pengganti tabel. getAllProducts() sudah ada. Isi insertProduct(name, price): buat { id: nextId++, name, price }, push ke products, lalu return produknya, seperti INSERT yang diikuti SELECT baris barunya. Kode awal mencetak:
Semua produk: 2
INSERT: {"id":3,"name":"Mouse","price":150000}
Setelah insert: 3
⚠️ Jebakan yang sering ketemu
- Menempel input ke teks SQL. Template literal
${...}atau+di dalam SQL membuka SQL injection. Selalu pakai?(atau$1di PostgreSQL). - Lupa
awaitdimysql2/promise.const [rows] = pool.execute(...)tanpaawaitgagal denganTypeError: object is not iterable (cannot read property Symbol(Symbol.iterator)), karena yang dikembalikan masih Promise. - Gak mengecek
undefineddari.get(). Baris yang gak ada ituundefined, bukan error. Jawab 404 sendiri. - Mengira restart menghapus database. Data di
catatan.dbtetap ada. Untuk mulai bersih, hapus file-nya.
TL;DR: Array hilang tiap server restart, database tetap. Node 24 punya SQLite bawaan: const { DatabaseSync } = require("node:sqlite"), new DatabaseSync("catatan.db"), CREATE TABLE IF NOT EXISTS, lalu db.prepare(sql).all(), .get(), .run() (yang mengembalikan changes dan lastInsertRowid). Nilai dari request selalu lewat placeholder ?, jangan ditempel ke teks SQL: id = 0 OR 1=1 yang ditempel menghapus semua baris (SQL injection). MySQL: mysql.createPool({...}) lalu const [rows] = await pool.execute("... WHERE id = ?", [id]). ORM populer: Prisma dan Sequelize, query builder: Knex.