
Validasi Input di Backend: Kenapa Never Trust the Client Bukan Sekadar Slogan
Validasi input di backend sering dianggap formalitas, padahal ini lapisan pertahanan terakhir API Anda. Ini pengalaman saya belajar lessons dari insiden produksi nyata dan pola validasi yang selalu saya pakai sekarang.
Beberapa tahun lalu, tim saya menemukan bug aneh di produksi: seorang pengguna berhasil mengubah harga item di keranjang belanjanya menjadi Rp 1. Aplikasi e-commerce B2B yang kami bangun punya validasi di frontend — input harga di-disable, hanya tampil. Tapi klien REST yang dipakai customer tersebut tidak peduli dengan UI kami. Dia tinggal buka Postman, salin token JWT-nya, dan kirim request langsung ke endpoint update-cart dengan payload harga: 1. Tanpa drama, tanpa eksploitasi canggih. Hanya satu field yang seharusnya tidak pernah dikirim klien.
Insiden itu mengubah cara saya memandang validasi input. "Never trust the client" bukan slogan keamanan yang dibaca-di-materi-onboarding. Ini konsekuensi arsitektur yang sangat praktis: HTTP API Anda adalah kontrak publik, dan siapa pun dengan token valid bisa mengirim apa pun ke sana.
Kenapa Validasi Frontend Tidak Pernah Cukup
Validasi di frontend punya satu tujuan sah: UX. Memberi feedback cepat sebelum user menekan submit itu bagus. Tapi frontend berjalan di mesin pengguna, pada browser yang bisa dimodifikasi, di environment yang tidak Anda kontrol.
Tiga jalur yang membuat validasi frontend bisa dilewati:
- Request langsung ke API. Seperti kasus saya di atas. DevTools, Postman, atau
curlbisa mengirim payload apa pun. - Modifikasi runtime. JavaScript bisa diubah via DevTools, validasi function bisa dioverride.
- Bug library. Salah satu contoh menarik: perilaku autofill Chrome yang menyulitkan kontrol penuh atas nilai form — ada diskusi panjang di Stack Overflow tentang disabling Chrome Autofill yang menunjukkan seberapa sedikit kontrol kita atas perilaku browser klien.
Saya pernah melihat tim yang menunda validasi backend dengan alasan "nanti setelah frontend stabil". Jangan. Validasi backend bukan fitur tambahan; ia adalah satu-satunya lapisan yang benar-benar milik Anda.

Validasi vs Sanitasi vs Normalisasi — Jangan Tukar-tukar
Kesalahan yang paling sering saya lihat (termasuk di kode saya sendiri jaman dulu): mencampur tiga hal ini jadi satu function raksasa sanitizeAndValidateEverything().
- Validasi: menolak data yang tidak sesuai ekspektasi. Umur harus angka 0–120. Salah? Tolak, 422.
- Sanitasi: membersihkan data berbahaya — escape output, strip tag HTML.
- Normalisasi: menyeragamkan format sebelum diproses — trim whitespace, lowercase email, uppercase kode.
Pertanyaan normalisasi bahkan muncul di level frontend — misalnya diskusi tentang memaksa input uppercase di Vue.js menunjukkan developer sering lupa bahwa transformasi UI tidak menjamin data yang sampai ke backend sudah seragam. Normalisasi tetap harus diulang di server.
Aturan praktis saya: tolak sejak dini, normalisasi setelah lolos validasi, sanitasi saat output — bukan saat input. Sanitasi input (misalnya strip HTML saat menyimpan) justru sering merusak data; yang benar adalah escape saat data dirender.
Contoh Nyata: Validasi dengan Zod di Endpoint Express
Ini pola yang sekarang saya pakai di hampir semua proyek TypeScript. Zod, schema di level HTTP handler, dan penolakan eksplisit field tak dikenal:
import { z } from 'zod';
const CreateOrderSchema = z.object({
productId: z.string().uuid(),
quantity: z.number().int().min(1).max(999),
// catatan: TIDAK ADA field `harga`
}).strict(); // .strict() menolak field tak dikenal
export async function createOrder(req: Request, res: Response) {
const parsed = CreateOrderSchema.safeParse(req.body);
if (!parsed.success) {
return res.status(422).json({
error: 'VALIDATION_ERROR',
details: parsed.error.flatten().fieldErrors,
});
}
// harga TIDAK PERNAH diambil dari request — selalu dari database
const product = await prisma.product.findUnique({
where: { id: parsed.data.productId },
});
if (!product || product.stock < parsed.data.quantity) {
return res.status(409).json({ error: 'PRODUCT_UNAVAILABLE' });
}
// ...
}
Dua hal penting di sini:
.strict()— default Zod membiarkan field ekstra lolos. Field tak dikenal sebaiknya ditolak, karena field tak dikenal adalah sinyal bug di klien atau upaya manipulasi.- Harga, total, dan diskon dihitung ulang di server. Data turunan tidak pernah dipercaya dari klien. Ini persis celah yang dipakai di insiden yang saya ceritakan di awal.
Pola validasi ini berdampingan baik dengan middleware Express.js yang saya pakai di produksi — error handler terpusat bisa memetakan ZodError ke response 422 yang konsisten tanpa try-catch di tiap handler.

Lapisan Terakhir yang Sering Dilupakan: Database
Validasi di application layer bukan akhir cerita. Saya selalu menambahkan constraint di database sebagai jaring pengaman:
ALTER TABLE orders
ADD CONSTRAINT quantity_positive CHECK (quantity > 0);
CHECK constraint, NOT NULL, foreign key, dan unique index menyelamatkan saya lebih dari sekali ketika ada bug di application layer — atau ketika ada script one-off yang menulis langsung ke database melewati API. Prisma mendukung ini via migrasi raw SQL; jangan hanya andalkan tipe TypeScript.
Ingat juga: validasi di level API dan level database punya tujuan berbeda. Schema API menjaga kontrak dengan klien; constraint database menjaga integritas data terlepas dari siapa penulisnya. Keduanya perlu.
Error Message: Jangan Bocorkan, Tapi Jangan Gabung Juga
Dua kesalahan berlawanan yang sama-sama sering saya temui saat review code:
- Mengembalikan
"Invalid input"generik untuk semua kasus — menyiksa klien yang mencoba integrasi dan memicu round-trip debugging berjam-jam. - Menempel
error.messagedari exception internal langsung ke response — bisa membocorkan detail schema, path query, bahkan connection string.
Jalan tengahnya: kembalikan error validasi yang terstruktur per-field (seperti fieldErrors di contoh Zod di atas), tapi pastikan error tak terduga di-catch oleh error handler global yang hanya mengembalikan "Internal server error" plus correlation ID ke log. Ini konsisten dengan prinsip desain REST API yang tangguh untuk produksi — error response adalah bagian dari kontrak API Anda.
Nuansa: Kapan Validasi Ketat Justru Menyusahkan
Saya tidak akan mengklaim .strict() plus reject-everything selalu benar. Ada trade-off nyata:
- API publik dengan klien pihak ketiga: menolak field tak dikenal akan merusak klien lama saat Anda menambah field baru di request. Untuk ini, toleransi field ekstra lebih aman daripada
.strict(). - Webhook dari vendor eksternal: payload sering berubah tanpa pengumuman. Validasi longgar di level "field yang saya butuhkan" lebih tahan banting.
- Form user-facing internal: di sini saya paling ketat —
.strict(), reject unknown, karena satu-satunya klien adalah aplikasi kita sendiri.
Jadi keputusannya tergantung siapa klien Anda dan seberapa besar kendali Anda atas mereka. Yang tidak bisa dinegosiasi: data turunan (harga, permission, ownership) selalu diverifikasi ulang di server.
Checklist yang Saya Jalankan Sebelum Merge Endpoint Baru
Setelah dua kali kena masalah lewat celah validasi, sekarang setiap endpoint baru di tim saya harus lolos pertanyaan-pertanyaan ini:
- Apakah ada data turunan (harga, role, userId) yang diambil dari request body? Kalau ya, refactor.
- Apakah ada schema validation dengan tipe eksplisit, atau hanya
req.body.hargalangsung dipakai? - Apakah ownership dicek — user ini benar-benar berhak mengakses resource dengan id itu? (Ini validasi juga, bukan hanya masalah autentikasi JWT vs session.)
- Apakah database punya constraint sebagai jaring pengaman?
- Apakah error validasi terstruktur dan tidak membocorkan detail internal?
Kalau Anda hanya bisa membawa satu hal dari artikel ini: buka satu endpoint Anda sekarang, dan cari satu field yang dipercaya langsung dari request padahal seharusnya diverifikasi server. Biasanya ada — dan lebih baik Anda yang menemukannya daripada orang lain.
Sumber
- Disabling Chrome Autofill — Stack Overflow — contoh konkret betapa terbatasnya kontrol kita atas perilaku browser klien, salah satu alasan validasi tidak bisa berhenti di frontend.
- Force input form enter uppercase to backend in Vue.js — Stack Overflow — ilustrasi bahwa normalisasi format di UI tidak menjamin data yang sampai ke backend sudah seragam.
Tomi Hartanto
Senior Software Engineer
Backend developer dengan 8+ tahun pengalaman membangun sistem scalable. Menulis tentang arsitektur backend, database, dan DevOps.
Artikel Terkait

N+1 Query Problem di Prisma dan Laravel Eloquent: Studi Kasus dan Cara Saya Memperbaikinya

Struktur Folder Proyek Backend yang Scalable: Peluang dari Sistem Enterprise yang Saya Kerjakan

Background Job dan Queue untuk Task Berat: Konsep dan Implementasi dengan BullMQ
Tomi Hartanto
Senior Software EngineerBackend developer dengan 8+ tahun pengalaman membangun sistem scalable. Menulis tentang arsitektur backend, database, dan DevOps.