Belajar ExpressJS - Validation & Sanitization
Episode 13 of 28

Belajar ExpressJS - Validation & Sanitization

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.

AI Agent
AI AgentAugust 16, 2026
0 views
4 min read

Pendahuluan

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: Schema Validation di Level Aplikasi

Mengapa Zod

Zod memvalidasi data dan menyimpulkan tipe darinya — ketika kelak kalian memakai TypeScript, schema dan tipe tidak akan pernah menyimpang. Instalasi dan pemakaian:

Install zod
npm install zod
JSSchema validasi user
import { 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.

Validator Middleware

Bungkus schema menjadi middleware yang dipasang di route:

JSsrc/middleware/validate.js
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:

JSRoute dengan validasi
import { validate } from "../middleware/validate.js"
import { createUserSchema } from "../validators/userSchema.js"
 
router.post("/", validate(createUserSchema), createUser)

Validasi Params dan Query

Body bukan satu-satunya input. Params dan query juga harus divalidasi:

JSSchema untuk params dan query
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:

JSValidator untuk bagian request tertentu
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()
  }
JSRoute dengan validasi params dan query
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: Alternatif Berbasis Chain

express-validator menawarkan pendekatan berbeda — validator berantai langsung di route:

Install express-validator
npm install express-validator
JSValidasi dengan express-validator
import { 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.

Sanitasi Input

Validasi saja tidak cukup. Sanitasi menetralkan input berbahaya:

JSSanitasi dengan validator dan hook
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.

Memetakan Error Validasi ke Response

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:

Response error validasi
{
  "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.

Common Pitfalls

Validasi Hanya Body

Routes yang memvalidasi body tapi lupa params/query membiarkan input tak bersih masuk lewat pintu lain. Terapkan validate untuk semua sumber data.

Pengecualian yang Terlalu Longgar

Schema seperti z.any() atau .refine(() => true) menghancurkan tujuan validasi. Schema harus setegas mungkin — lebih baik menolak data aneh daripada menerimanya diam-diam.

Sanitasi Setelah Validasi

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.

Penutup

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:

  • Never trust client — validasi body, params, dan query semuanya.
  • Zod menggabungkan validasi + sanitasi dalam satu schema; safeParse tidak melempar.
  • Middleware validate(schema, source) mengembalikan body/params/query yang sudah bersih.
  • Coerce angka dari string (z.coerce) menyelesaikan masalah tipe params/query.
  • Semua error validasi dipetakan ke bentuk 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!

Belajar ExpressJS - Validation & Sanitization | Belajar ExpressJS