Daftar buku Toko Buku sudah diambil dari database. Sekarang client mau memilih: buku Andrea Hirata saja, yang termahal dulu, halaman 2. Pilihan itu dikirim lewat query string, bagian URL sesudah tanda ?: GET /books?author_id=1&sort=price&order=desc. Filter menyaring baris mana yang ikut, sort mengatur urutannya. Bayangin minta tolong ke pelayan toko: "yang Andrea Hirata saja, urutkan dari yang paling mahal". Pelayan cuma melayani pilihan yang ada di daftarnya. Mengurutkan menurut judul, harga, atau stok boleh. Menurut harga modal gak boleh, itu rahasia toko.
🧪 Tebakan cepat: handler mengurutkan dengan
ORDER BY ?dan mengirim nilaisortlewat placeholder, sama seperti nilai filter. Request?sort=pricehasilnya terurut menurut harga, tetap urutan asal, atau error? Tebak dulu, jawabannya ada di bagian "Sort: nama kolom gak bisa lewat placeholder".
Membaca query string
Express membaca query string ke req.query. Isinya selalu teks, dan parameter yang gak dikirim bernilai undefined:
| Request | req.query |
|---|---|
GET /books |
{} |
GET /books?author_id=1 |
{ author_id: "1" } |
GET /books?author_id=1&sort=price&order=desc |
{ author_id: "1", sort: "price", order: "desc" } |
Di Laravel sama: GET /api/siswa?kelas=XII dibaca di controller dengan $request->query('kelas'), hasilnya "XII".
Filter: syarat ditambah satu per satu
Tiap filter yang dikirim menambah satu syarat ke WHERE. Filter yang gak dikirim gak menambah apa-apa.
const where = [];
const params = [];
if (req.query.author_id) {
where.push("author_id = ?");
params.push(Number(req.query.author_id));
}
if (req.query.in_stock === "1") {
where.push("stock > 0");
}
const whereSql = where.length ? " WHERE " + where.join(" AND ") : "";
- Teks syaratnya (
"author_id = ?","stock > 0") ditulis server sendiri. Nilai dari client cuma masuk keparams, lewat placeholder, seperti di pelajaran Query Database. where.join(" AND ")menyambung syarat-syaratnya: semua harus benar.in_stockdibandingkan dengan"1", bukan dicek truthy.?in_stock=falseitu teks"false", dan teks yang gak kosong selalu truthy.
| Query string | WHERE yang jadi |
params |
Buku |
|---|---|---|---|
| (kosong) | (gak ada) | [] |
8 |
?author_id=1 |
WHERE author_id = ? |
[1] |
3 |
?in_stock=1 |
WHERE stock > 0 |
[] |
6 |
?author_id=2&in_stock=1 |
WHERE author_id = ? AND stock > 0 |
[2] |
1 |
Sort: nama kolom gak bisa lewat placeholder
Placeholder cuma mengisi nilai. ORDER BY ? dengan nilai "price" artinya "urutkan menurut teks 'price'", dan teks itu sama persis di semua baris. SQLite gak error, tapi gak mengurutkan apa-apa: hasilnya tetap urutan asal, Laskar Pelangi, Bumi Manusia, Cantik Itu Luka, dan seterusnya. Itu jawaban tebakannya.
Jadi nama kolom harus ditulis ke teks SQL. Satu-satunya cara aman: allowlist, daftar nama yang diizinkan, ditulis server sendiri.
const SORTABLE = ["id", "title", "price", "stock"];
const { sort = "id", order = "asc" } = req.query;
if (!SORTABLE.includes(sort)) {
return res.status(400).json({ error: "sort must be one of: id, title, price, stock" });
}
if (order !== "asc" && order !== "desc") {
return res.status(400).json({ error: "order must be asc or desc" });
}
const orderSql = ` ORDER BY ${sort} ${order.toUpperCase()}`;
sort = "id"danorder = "asc"adalah nilai default kalau client gak mengirimnya.SORTABLE.includes(sort)memastikansortadalah salah satu dari empat nama yang ditulis server. Baru sesudah itu dia boleh masuk ke SQL.ascartinya naik (kecil ke besar, A ke Z),descartinya turun.- Nilai di luar daftar dijawab
400, bukan diabaikan diam-diam. Client yang salah ketiksort=hargajadi langsung tahu.
Kenapa harus begini? Tanpa allowlist, ?sort=cost_price mengurutkan menurut harga modal: Edensor (modal 52.000) di depan, lalu Sang Pemimpi (55.000). Padahal menurut harga jual, Sang Pemimpi (79.000) yang termurah. API gak pernah mengirim cost_price, tapi urutannya membocorkannya. Dan karena sort masuk ke teks SQL, isi lain bisa mengubah perintahnya: SQL injection.
Halaman: page dan limit
LIMIT dan OFFSET adalah nilai, jadi boleh lewat placeholder. limit dijaga antara 1 dan 50, supaya client gak bisa meminta sejuta baris sekaligus. Satu query lagi menghitung total dengan WHERE yang sama, untuk meta seperti di pelajaran Pagination. Handler lengkapnya:
const SORTABLE = ["id", "title", "price", "stock"];
app.get("/books", (req, res) => {
const { author_id, in_stock, sort = "id", order = "asc" } = req.query;
if (!SORTABLE.includes(sort)) {
return res.status(400).json({ error: "sort must be one of: id, title, price, stock" });
}
if (order !== "asc" && order !== "desc") {
return res.status(400).json({ error: "order must be asc or desc" });
}
const page = Math.max(parseInt(req.query.page, 10) || 1, 1);
const limit = Math.min(Math.max(parseInt(req.query.limit, 10) || 3, 1), 50);
const where = [];
const params = [];
if (author_id) {
where.push("author_id = ?");
params.push(Number(author_id));
}
if (in_stock === "1") where.push("stock > 0");
const whereSql = where.length ? " WHERE " + where.join(" AND ") : "";
const { total } = db.prepare("SELECT COUNT(*) AS total FROM books" + whereSql).get(...params);
const data = db
.prepare(`SELECT id, title, author_id, price, stock FROM books${whereSql} ORDER BY ${sort} ${order.toUpperCase()} LIMIT ? OFFSET ?`)
.all(...params, limit, (page - 1) * limit);
res.json({ data, meta: { page, limit, total, totalPages: Math.ceil(total / limit) } });
});
GET /books?author_id=1&sort=price&order=desc dijawab tiga buku Andrea Hirata, termahal dulu: Laskar Pelangi (89.000), Edensor (85.000), Sang Pemimpi (79.000), plus "meta": { "page": 1, "limit": 3, "total": 3, "totalPages": 1 }.
GET /books?in_stock=1&sort=title&page=2: 6 buku lolos filter, limit 3, jadi totalPages 2. Halaman 2 berisi Laskar Pelangi, Ronggeng Dukuh Paruk, dan Sang Pemimpi.
📌 Di Laravel
$kelas = $request->query('kelas');
$sort = $request->query('sort', 'id');
abort_unless(in_array($sort, ['id', 'nama', 'nis'], true), 400, 'sort must be one of: id, nama, nis');
$order = $request->query('order') === 'desc' ? 'desc' : 'asc';
$siswa = Siswa::when($kelas, fn ($q) => $q->where('kelas', $kelas))
->orderBy($sort, $order)
->paginate(15);
when($kelas, ...)menambahwherehanya kalau$kelasada isinya. Tanpa?kelas=, semua siswa ikut.orderBy($request->query('sort'))tanpa allowlist punya risiko yang sama: SQL injection atau akses kolom sensitif.in_array(..., true)adalah allowlist-nya.
⚠️ Jebakan yang sering ketemu
- Nama kolom lewat placeholder.
ORDER BY ?gak error, tapi gak mengurutkan. Pakai allowlist. - Nama kolom langsung dari client.
`ORDER BY ${req.query.sort}`tanpa cek membuka SQL injection dan kolom rahasia seperticost_price. - Nilai filter disambung ke SQL.
"WHERE author_id = " + req.query.author_idsama bahayanya dengan di pelajaran sebelumnya. Nilai selalu lewat?. - Cek truthy untuk teks.
if (req.query.in_stock)meloloskan"false"dan"0". Bandingkan dengan nilai yang kamu maksud. - Sort salah diabaikan diam-diam. Client gak pernah tahu
sort=hargasalah ketik. Jawab400dengan daftar nilai yang boleh.
TL;DR: Filter dan sort dikirim lewat query string, ?author_id=1&sort=price&order=desc, dan dibaca dari req.query (di Laravel $request->query('kelas')); semua nilainya teks. Tiap filter menambah satu syarat ke WHERE, dengan nilai lewat placeholder. Nama kolom gak bisa lewat placeholder: ORDER BY ? gak mengurutkan apa-apa. Jadi sort dicek dengan allowlist (["id", "title", "price", "stock"]) dan order harus asc atau desc; selain itu dijawab 400. Tanpa allowlist, sort=cost_price membocorkan harga modal lewat urutan, dan orderBy($column) dari client membuka SQL injection atau akses kolom sensitif. LIMIT ? OFFSET ? plus meta untuk halaman. Di Laravel, when($kelas, ...) menambah filter hanya kalau parameternya ada.