Debug bug coercion JavaScript yang merusak validasi backend biasanya bermula dari asumsi yang keliru: data yang terlihat seperti angka atau boolean dianggap sudah aman dipakai di Node/Express. Padahal, request HTTP membawa banyak nilai sebagai string, dan JavaScript punya aturan coercion yang kadang membantu, kadang diam-diam merusak logika.
Dalam backend, efeknya tidak berhenti di layer validasi. Nilai seperti "false", "0", " ", atau "10abc" bisa lolos pengecekan seadanya, lalu mengubah query database, menyalakan flag bisnis yang seharusnya mati, atau menyimpan data korup. Fokus artikel ini adalah studi kasus praktis di service Node/Express, bukan sekadar teori bahasa.
Secara historis, JavaScript memang lahir dengan banyak kompromi demi kompatibilitas dan kemudahan adopsi. Presentasi The Birth & Death of JavaScript dari Destroy All Software sering dipakai untuk mengingatkan bahwa banyak perilaku bahasa ini masuk akal dari sisi sejarah, tetapi berbahaya bila dipakai tanpa validasi eksplisit di backend.
Studi kasus: validasi lolos, status publikasi berubah sendiri
Misalkan ada endpoint Express untuk memperbarui produk:
PATCH /products/:idBackend menerima payload seperti berikut:
{
"stock": "0",
"isPublished": "false",
"price": "15000"
}Secara bisnis, arti payload di atas jelas:
stockharus menjadi angka0isPublishedharus menjadi booleanfalsepriceharus menjadi angka15000
Namun implementasi awal backend sering terlihat seperti ini:
app.patch('/products/:id', async (req, res, next) => {
try {
const { stock, isPublished, price } = req.body;
if (!stock || !price) {
return res.status(400).json({ message: 'stock dan price wajib diisi' });
}
const update = {
stock: Number(stock),
price: Number(price),
is_published: !!isPublished
};
await db('products')
.where({ id: req.params.id })
.update(update);
res.json({ ok: true });
} catch (err) {
next(err);
}
});Kode ini terlihat sederhana, tetapi menyimpan beberapa bug coercion sekaligus.
Gejala di production
- Produk dengan
stock = 0ditolak dengan pesan field wajib diisi. - Produk yang dikirim dengan
isPublished: "false"justru tersimpan sebagaitrue. - Nilai
price: "15abc"kadang lolos lebih jauh bila validasinya tidak ketat dan baru gagal saat menyentuh database atau perhitungan lain. - Log aplikasi menunjukkan payload “kelihatan benar”, tetapi data di database berbeda dari niat klien.
Reproduksi bug dengan payload pemicu
Berikut beberapa request yang realistis untuk mereproduksi masalah.
Kasus 1: boolean string berubah jadi true
curl -X PATCH http://localhost:3000/products/42 \
-H "Content-Type: application/json" \
-d '{"stock":"10","price":"15000","isPublished":"false"}'Jika backend memakai !!isPublished, maka hasilnya true karena semua string non-kosong di JavaScript adalah truthy, termasuk string "false".
Kasus 2: angka 0 dianggap tidak ada
curl -X PATCH http://localhost:3000/products/42 \
-H "Content-Type: application/json" \
-d '{"stock":0,"price":15000,"isPublished":false}'Pengecekan if (!stock) akan menganggap 0 sebagai false. Akibatnya, field yang valid justru ditolak.
Kasus 3: query string boolean menyalakan filter yang salah
Kasus lain sering muncul di endpoint pencarian:
GET /orders?includeArchived=falseImplementasi yang rawan:
app.get('/orders', async (req, res, next) => {
try {
const includeArchived = !!req.query.includeArchived;
const query = db('orders');
if (!includeArchived) {
query.where({ archived: false });
}
const rows = await query;
res.json(rows);
} catch (err) {
next(err);
}
});Nilai req.query.includeArchived biasanya berupa string. Saat klien mengirim ?includeArchived=false, nilai string "false" menjadi truthy, sehingga includeArchived bernilai true. Hasilnya, order terarsip ikut tampil walau klien secara eksplisit meminta sebaliknya.
Root cause: coercion terjadi di parsing, validasi, dan konversi
1. HTTP input tidak selalu bertipe seperti yang kita bayangkan
Meski body JSON bisa mempertahankan tipe asli bila klien mengirim boolean atau number yang benar, banyak klien tetap mengirim string untuk field numerik dan boolean. Untuk query string, hampir semua nilai masuk sebagai string. Jadi asumsi bahwa false akan selalu datang sebagai boolean adalah sumber bug.
2. Truthy/falsy bukan validasi tipe
Pola seperti ini sangat umum dan berbahaya:
if (!value) { ... }Pengecekan tersebut bukan memeriksa “field tidak ada”, tetapi memeriksa apakah nilainya falsy. Nilai berikut akan ikut dianggap gagal:
0false""nullundefinedNaN
Dalam validasi backend, beberapa di antaranya valid secara bisnis. Misalnya, stock = 0 dan isPublished = false.
3. Double negation (!!value) bukan parser boolean
!!value hanya mengubah nilai menjadi truthy/falsy boolean, bukan membaca arti string. Contoh:
!!"false" // true
!!"0" // true
!!"no" // true
!!"" // falseKalau sumber input berasal dari request, perilaku ini hampir selalu salah untuk field boolean semantik.
4. Number(), parseInt(), dan validasi longgar
Konversi angka juga punya jebakan:
Number(" ")menghasilkan0Number("")menghasilkan0Number("10abc")menghasilkanNaNparseInt("10abc", 10)menghasilkan10
Bila backend hanya memeriksa “berhasil dikonversi ke angka” secara longgar, input parsial atau kosong bisa lolos dan menghasilkan data yang tidak diinginkan.
Langkah investigasi saat bug muncul di backend
Saat melihat data di database tidak sesuai dengan payload, jangan langsung menyalahkan ORM atau database. Mulai dari observasi tipe dan jalur transformasi data.
1. Log nilai dan tipenya
Logging yang hanya menampilkan nilai sering menyesatkan. Tambahkan tipe eksplisit:
logger.info('incoming payload', {
stock: req.body.stock,
stockType: typeof req.body.stock,
isPublished: req.body.isPublished,
isPublishedType: typeof req.body.isPublished,
price: req.body.price,
priceType: typeof req.body.price
});Ini sering langsung mengungkap bahwa field yang dikira boolean ternyata string.
2. Pisahkan tahap parsing, validasi, dan persistence
Bug coercion sering sulit dilacak karena semua dilakukan sekaligus di controller. Pecah alurnya:
- Ambil raw input dari request
- Validasi schema input
- Transformasi ke tipe internal yang jelas
- Jalankan logika bisnis
- Simpan ke database
Dengan struktur ini, Anda bisa tahu bug muncul di tahap mana.
3. Reproduksi dengan test atau curl yang spesifik
Jangan memakai payload “normal” saja. Uji nilai yang memang sering memicu coercion:
false,"false"0,"0"""," "null,undefinedbila relevan"10abc","01"
4. Periksa apakah bug datang dari parser middleware atau framework layer
Untuk body JSON, cek bentuk data setelah middleware parser. Untuk query string, ingat bahwa nilainya lazim berupa string. Masalahnya sering bukan pada Express, melainkan pada asumsi aplikasi setelah parsing request.
Perbaikan kode: validasi eksplisit dan transformasi yang ketat
Prinsip utamanya: jangan andalkan coercion implisit untuk input eksternal. Terapkan parser dan validator yang jelas untuk setiap field.
Contoh perbaikan manual tanpa schema library
function parseBooleanStrict(value) {
if (value === true || value === false) return value;
if (value === 'true') return true;
if (value === 'false') return false;
throw new Error('Nilai boolean tidak valid');
}
function parseNumberStrict(value, fieldName) {
if (typeof value === 'number') {
if (!Number.isFinite(value)) {
throw new Error(`${fieldName} harus angka finite`);
}
return value;
}
if (typeof value === 'string') {
if (value.trim() === '') {
throw new Error(`${fieldName} tidak boleh kosong`);
}
const num = Number(value);
if (!Number.isFinite(num)) {
throw new Error(`${fieldName} harus angka valid`);
}
return num;
}
throw new Error(`${fieldName} harus berupa number atau string numerik`);
}
app.patch('/products/:id', async (req, res, next) => {
try {
if (!Object.prototype.hasOwnProperty.call(req.body, 'stock')) {
return res.status(400).json({ message: 'stock wajib dikirim' });
}
if (!Object.prototype.hasOwnProperty.call(req.body, 'price')) {
return res.status(400).json({ message: 'price wajib dikirim' });
}
if (!Object.prototype.hasOwnProperty.call(req.body, 'isPublished')) {
return res.status(400).json({ message: 'isPublished wajib dikirim' });
}
const stock = parseNumberStrict(req.body.stock, 'stock');
const price = parseNumberStrict(req.body.price, 'price');
const isPublished = parseBooleanStrict(req.body.isPublished);
if (!Number.isInteger(stock) || stock < 0) {
return res.status(400).json({ message: 'stock harus integer >= 0' });
}
if (price < 0) {
return res.status(400).json({ message: 'price harus >= 0' });
}
await db('products')
.where({ id: req.params.id })
.update({
stock,
price,
is_published: isPublished
});
res.json({ ok: true });
} catch (err) {
if (err.message.includes('tidak valid') || err.message.includes('harus')) {
return res.status(400).json({ message: err.message });
}
next(err);
}
});Ada beberapa perbaikan penting di sini:
- Tidak memakai
if (!stock)untuk mengecek keberadaan field. - Tidak memakai
!!valueuntuk membaca boolean. - Angka kosong seperti
""atau" "ditolak, bukan diam-diam diubah menjadi0. - Validasi domain bisnis dipisahkan dari konversi tipe.
Mengapa pendekatan ini lebih aman
Kode di atas memperjelas kontrak API. Backend tidak menebak maksud klien dari coercion implisit. Sebaliknya, backend menerima hanya bentuk input yang memang didukung. Hasilnya lebih mudah diuji, lebih mudah di-debug, dan mengurangi korupsi data diam-diam.
Penguatan dengan schema validation
Untuk tim yang mengelola banyak endpoint, validasi manual akan cepat berulang. Lebih baik gunakan schema validation di boundary request. Apa pun library yang dipilih, targetnya sama:
- definisikan field wajib dan opsional secara eksplisit,
- bedakan antara parsing dan validation,
- tentukan apakah string numerik boleh diterima atau harus ditolak,
- tentukan representasi boolean yang valid.
Prinsip schema yang sebaiknya diterapkan
- Strict object: tolak field tak dikenal bila endpoint sensitif.
- Required vs optional: jangan pakai truthy/falsy untuk menentukan keberadaan field.
- No implicit boolean coercion:
"false"harus diperlakukan sesuai aturan yang Anda definisikan, bukan sekadar truthy. - Numeric constraints: cek integer, minimum, maksimum, dan finite number.
- Consistent error response: kembalikan pesan yang menunjukkan field mana yang gagal dan kenapa.
Kapan menerima string numerik?
Ini trade-off penting. Ada dua pendekatan yang sama-sama valid tergantung konteks API:
- Ketat: hanya terima number asli untuk body JSON. Cocok untuk internal API yang konsumennya terkontrol.
- Pragmatis: terima string numerik lalu parse secara ketat. Cocok bila banyak klien lama atau integrasi pihak ketiga mengirim data tidak konsisten.
Yang berbahaya bukan memilih salah satunya, tetapi menerima keduanya tanpa aturan yang jelas.
Dampak bug coercion ke database dan logika bisnis
Masalah ini sering terlihat sepele karena hanya “tipe data”, padahal dampaknya bisa operasional:
- Data salah tersimpan: flag publikasi, status aktif, atau soft-delete berubah tanpa niat.
- Filter query rusak: data yang harusnya tersembunyi menjadi tampil.
- Perhitungan salah: subtotal, diskon, atau stok memakai angka hasil konversi yang tidak semestinya.
- Audit sulit: payload log tampak benar, tetapi transformasi internal mengubah maknanya.
- Bug intermittent: hanya muncul untuk klien tertentu yang mengirim string alih-alih boolean/number asli.
Karena itu, bug coercion sering lolos dari pengujian dangkal tetapi memukul production saat ada variasi payload nyata dari frontend, admin panel, script integrasi, atau partner API.
Logging yang benar-benar membantu saat debugging
Logging untuk kasus coercion harus dirancang agar menjelaskan transformasi, bukan sekadar snapshot akhir.
Apa yang perlu dilog
- Request ID atau correlation ID
- Raw input terpilih yang aman untuk dicatat
- Tipe setiap field penting
- Hasil parsing setelah validation layer
- SQL/query intent atau parameter persistence bila aman
- Error validation dengan path field yang jelas
Contoh pola log yang berguna
logger.info('product update input', {
requestId,
raw: {
stock: req.body.stock,
isPublished: req.body.isPublished,
price: req.body.price
},
rawTypes: {
stock: typeof req.body.stock,
isPublished: typeof req.body.isPublished,
price: typeof req.body.price
}
});
logger.info('product update parsed', {
requestId,
parsed: {
stock,
isPublished,
price
}
});Dengan dua log ini, Anda bisa melihat perbedaan antara input klien dan state internal setelah parsing. Itu sangat membantu untuk mengisolasi akar masalah.
Test yang wajib ditambahkan setelah perbaikan
Begitu bug ditemukan, ubah jadi test agar tidak kembali lagi.
Daftar kasus uji minimum
stock = 0diterima bila valid secara bisnis.isPublished = falsetidak berubah menjaditrue.isPublished = "false"diproses sesuai kontrak API: diterima dan diparse, atau ditolak jelas.price = ""ditolak.price = " "ditolak.price = "15abc"ditolak.- Query string
?includeArchived=falsebenar-benar memfilter data aktif saja.
Fokus pada kontrak, bukan implementasi internal
Test sebaiknya memverifikasi respons API dan efek ke database, bukan sekadar memeriksa fungsi helper. Dengan begitu, jika nanti validasi dipindahkan ke middleware atau schema library, test tetap relevan.
Checklist pencegahan untuk tim backend
- Anggap semua input request tidak tepercaya, termasuk dari frontend internal.
- Jangan gunakan truthy/falsy untuk required check pada field numerik atau boolean.
- Jangan gunakan
!!valueuntuk parse boolean request. - Pisahkan parsing, validation, dan business logic.
- Definisikan kontrak API secara eksplisit: apakah body menerima string numerik atau tidak.
- Standarkan schema validation di semua endpoint baru.
- Log tipe input pada field sensitif saat debugging atau pada level observability yang sesuai.
- Tambahkan test untuk nilai batas:
0,false,"false", string kosong, dan input parsial. - Review code smell umum seperti
if (!value),!!req.query.flag, atauparseInttanpa validasi tambahan. - Pastikan layer database punya constraint bila memungkinkan, agar input salah tidak mudah menjadi data korup.
Penutup
Bug coercion JavaScript yang merusak validasi backend biasanya bukan bug besar yang dramatis, melainkan bug kecil yang diam-diam mengubah arti data. Justru karena terlihat sepele, bug ini sering lolos code review dan baru terasa saat logika bisnis atau isi database mulai menyimpang.
Di Node/Express, solusi yang paling efektif adalah disiplin pada boundary input: validasi schema yang tegas, parsing eksplisit untuk boolean dan number, logging tipe saat investigasi, dan test untuk payload yang memang rawan coercion. Dengan begitu, perilaku historis JavaScript tetap bisa dikelola tanpa merusak kontrak API backend Anda.
Komentar
0 komentar
Masuk ke akun kamu untuk ikut berkomentar.
Belum ada komentar
Jadilah yang pertama ikut berdiskusi!