Memvalidasi dan membersihkan input pengguna dengan zod dan express-validator, memvalidasi body params dan query, sanitasi, serta memetakan error validasi menjadi response 400 yang konsisten.

Setelah di episode 12 kalian mengamankan identitas, episode 13 menutup celah yang paling sering dieksploitasi: input. Prinsip dasar keamanan adalah never trust client — seluruh data dari body, params, dan query adalah musuh potensial sampai terbukti bersih.
Mengapa validasi dan sanitasi dibahas bersama? Karena keduanya berbeda tapi saling melengkapi: validasi menolak data yang tidak memenuhi kontrak (tipe, format, rentang), sanitasi membersihkan data yang valid tapi berbahaya (strip tag HTML, normalisasi string). Mengabaikan keduanya membuka pintu ke NoSQL injection, SQL injection, dan XSS yang akan kita tutup penuh di episode 18.
Zod memvalidasi data dan menyimpulkan tipe darinya — ketika kelak kalian memakai TypeScript, schema dan tipe tidak akan pernah menyimpang. Instalasi dan pemakaian:
npm install zodimport { z } from "zod"
export const createUserSchema = z.object({
name: z
.string()
.trim()
.min(2, "Nama minimal 2 karakter")
.max(100, "Nama maksimal 100 karakter"),
email: z.string().trim().toLowerCase().email("Email tidak valid"),
age: z.coerce.number().int().min(0).max(150).optional(),
role: z.enum(["admin", "editor", "viewer"]).default("viewer"),
})Perhatikan metode sanitasi bawaan: trim() membersihkan spasi, toLowerCase() menormalkan email, dan z.coerce.number() mengubah string "25" dari query menjadi angka 25. Validasi dan sanitasi menyatu di satu schema.
Bungkus schema menjadi middleware yang dipasang di route:
import { z } from "zod"
export const validate = (schema) => (req, res, next) => {
const result = schema.safeParse(req.body)
if (!result.success) {
const errors = result.error.issues.map((issue) => ({
field: issue.path.join("."),
message: issue.message,
}))
throw new AppError(400, "VALIDATION_ERROR", JSON.stringify(errors))
}
req.body = result.data
next()
}Dua hal penting dalam middleware ini:
safeParse tidak melempar error — ia mengembalikan { success, data }, sehingga bisa dibaca hasilnya.req.body = result.data menggantikan body asli dengan hasil validasi (yang sudah di-trim/normalisasi) — handler di bawahnya hanya melihat data bersih.Pemakaian di route:
import { validate } from "../middleware/validate.js"
import { createUserSchema } from "../validators/userSchema.js"
router.post("/", validate(createUserSchema), createUser)Body bukan satu-satunya input. Params dan query juga harus divalidasi:
export const idParamsSchema = z.object({
id: z.coerce.number().int().positive("id harus positif"),
})
export const listUsersQuerySchema = z.object({
page: z.coerce.number().int().min(1).default(1),
limit: z.coerce.number().int().min(1).max(100).default(10),
search: z.string().trim().max(50).optional(),
})Middleware validasi dibuat parametrik agar bisa menargetkan bagian request:
export const validate =
(schema, source = "body") =>
(req, res, next) => {
const result = schema.safeParse(req[source])
if (!result.success) {
const errors = result.error.issues.map((issue) => ({
field: issue.path.join("."),
message: issue.message,
}))
throw new AppError(400, "VALIDATION_ERROR", JSON.stringify(errors))
}
req[source] = result.data
next()
}router.get("/", validate(listUsersQuerySchema, "query"), listUsers)
router.get("/:id", validate(idParamsSchema, "params"), getUser)Pola ini menyelesaikan masalah yang kita tanggung sejak episode 4 dan 6: nilai params/query yang selalu string kini dikoersi dan divalidasi di satu tempat.
express-validator menawarkan pendekatan berbeda — validator berantai langsung di route:
npm install express-validatorimport { body, validationResult } from "express-validator"
router.post(
"/",
body("name").trim().isLength({ min: 2, max: 100 }).withMessage("Nama tidak valid"),
body("email").trim().isEmail().withMessage("Email tidak valid"),
(req, res, next) => {
const errors = validationResult(req)
if (!errors.isEmpty()) {
throw new AppError(400, "VALIDATION_ERROR", JSON.stringify(errors.array()))
}
next()
}
)Kapan memilih yang mana? Zod lebih baik untuk kontrak data yang dipakai ulang (termasuk untuk meng-generate tipe TypeScript); express-validator lebih ringkas untuk validasi satu-off di route. Keduanya sah — konsistensi di dalam satu proyek yang paling penting.
Validasi saja tidak cukup. Sanitasi menetralkan input berbahaya:
export const profileSchema = z.object({
bio: z
.string()
.trim()
.max(500)
.transform((value) =>
value.replace(/<script[\s\S]*?>[\s\S]*?<\/script>/gi, "")
),
website: z.string().trim().url().max(200).optional(),
})Contoh di atas melakukan tiga hal: membatasi panjang, memotong tag <script> (lawan XSS), dan memaksa website menjadi URL valid. Di level Mongoose, schema episode 9 juga sudah melakukan sanitasi dasar (trim, lowercase) — dua lapis ini saling menguatkan.
Important
Validasi + sanitasi tidak sama dengan enkripsi. Keduanya membentuk lapisan input hygiene, sementara pertahanan terakhir tetap di database (parameterized query, escaped output). Kombinasi inilah yang kita rakit jadi strategi penuh di episode 18.
Error validasi dari Zod/express-validator berbentuk array issue. Agar konsisten dengan kontrak episode 6-7, kita menyeragamkan lewat AppError(400, "VALIDATION_ERROR", ...) — error handler episode 7 yang mengubahnya menjadi:
{
"error": "VALIDATION_ERROR",
"message": "[{\"field\":\"email\",\"message\":\"Email tidak valid\"}]"
}Untuk produksi, format message sebaiknya bukan JSON string tapi array tersendiri — sesuaikan AppError untuk membawa properti errors array yang di-serialize langsung oleh error handler. Intinya: satu bentuk error, dihasilkan dari satu middleware, dikirim oleh satu handler.
Routes yang memvalidasi body tapi lupa params/query membiarkan input tak bersih masuk lewat pintu lain. Terapkan validate untuk semua sumber data.
Schema seperti z.any() atau .refine(() => true) menghancurkan tujuan validasi. Schema harus setegas mungkin — lebih baik menolak data aneh daripada menerimanya diam-diam.
Men-sanitasi di handler yang tersebar menghasilkan kode yang tidak konsisten. Lakukan di schema (transform Zod) atau middleware — satu sumber kebenaran.
Tip
Kombinasikan dengan pitfall episode 9-10: isValidObjectId dan Number(req.params.id) di handler kini digantikan middleware validasi params. Satu lapisan, semua endpoint bersih — dan 400 otomatis untuk id acak.
Episode 13 menutup pintu input: schema Zod dengan sanitasi trim/toLowerCase/transform, middleware validate parametrik untuk body/params/query, express-validator sebagai alternatif, dan pemetaan error ke VALIDATION_ERROR yang konsisten.
Inti yang harus dibawa pulang:
safeParse tidak melempar.validate(schema, source) mengembalikan body/params/query yang sudah bersih.VALIDATION_ERROR yang konsisten.Di episode 14 selanjutnya kita akan menambahkan komunikasi realtime: WebSocket dengan Socket.IO — integrasi dengan Express, rooms, emit dan broadcast, serta realtime chat. Sampai jumpa di episode 14!