Validation #

Validation adalah garis pertahanan pertama sebelum data menyentuh business logic dan database. Tapi dalam praktik, validation sering diperlakukan sebagai afterthought — ditambahkan setelah fitur selesai, atau hanya menutupi kasus paling obvious. Akibatnya, bug yang seharusnya tertangkap di lapisan validasi malah lolos ke database sebagai data anomali, atau lebih buruk, dieksploitasi sebagai celah keamanan. Artikel ini membahas validation secara menyeluruh: tujuh layer validasi yang perlu ada, mengapa client-side validation tidak pernah cukup, bagaimana membangun validation yang terpisah dan testable, dan anti-pattern apa yang sering menjadi akar masalah.

Mengapa Validation Lebih dari Sekadar “Required Field” #

Ketika developer berbicara tentang validation, yang sering terpikirkan hanyalah cek apakah field wajib diisi atau apakah format email valid. Padahal validation yang komprehensif mencakup tujuh layer berbeda yang masing-masing melindungi hal yang berbeda.

flowchart TD
    Input["Input dari Client"]

    L1["Structural Validation\nApakah bentuk datanya benar?\n(field ada, tipe data sesuai)"]
    L2["Format Validation\nApakah formatnya sesuai?\n(email, UUID, tanggal)"]
    L3["Length & Size Validation\nApakah ukurannya wajar?\n(max length, max array size)"]
    L4["Value Constraint Validation\nApakah nilainya dalam rentang izin?\n(enum, range angka)"]
    L5["Business / Semantic Validation\nApakah masuk akal secara domain?\n(saldo cukup, tanggal tidak overlap)"]
    L6["Authorization Validation\nApakah user boleh melakukan ini?\n(ownership, role check)"]
    L7["Contextual Validation\nApakah valid dalam konteks ini?\n(conditional required field)"]

    BL["Business Logic"]
    DB["Database"]

    Input --> L1
    L1 --> L2
    L2 --> L3
    L3 --> L4
    L4 --> L5
    L5 --> L6
    L6 --> L7
    L7 --> BL
    BL --> DB

    style L1 fill:#2980B9,color:#fff
    style L2 fill:#27AE60,color:#fff
    style L3 fill:#F39C12,color:#fff
    style L4 fill:#E67E22,color:#fff
    style L5 fill:#E74C3C,color:#fff
    style L6 fill:#8E44AD,color:#fff
    style L7 fill:#2C3E50,color:#fff

Layer 1 — Structural Validation #

Structural validation adalah yang paling fundamental: apakah data yang masuk memiliki bentuk yang benar? Ini termasuk keberadaan field wajib, tipe data yang sesuai, dan structure JSON yang valid.

// ANTI-PATTERN: Tidak ada structural validation
func CreateOrder(r *http.Request) {
    var order Order
    json.NewDecoder(r.Body).Decode(&order)  // tidak cek apakah decode berhasil
    // order.Items bisa nil, order.UserID bisa zero value
    db.CreateOrder(order)  // data tidak lengkap masuk ke DB
}

// BENAR: Structural validation dengan strict decoding
type CreateOrderInput struct {
    Items    []OrderItem `json:"items"`
    Address  string      `json:"address"`
}

func CreateOrder(r *http.Request) {
    decoder := json.NewDecoder(r.Body)
    decoder.DisallowUnknownFields()  // reject field yang tidak dikenal

    var input CreateOrderInput
    if err := decoder.Decode(&input); err != nil {
        respondError(w, 400, "FORMAT_ERROR", "Format request tidak valid")
        return
    }

    // Lanjut ke validasi berikutnya
}

DisallowUnknownFields() adalah satu baris yang sering dilupakan tapi sangat berharga. Ia menolak request yang mengandung field tidak dikenal, yang merupakan pertahanan pertama terhadap mass assignment attack.


Layer 2 — Format Validation #

Format validation memastikan nilai yang diberikan mengikuti pola yang diharapkan. Ini bukan tentang kebenaran bisnis — email [email protected] mungkin valid secara format tapi tidak ada pemiliknya. Format validation hanya memastikan strukturnya masuk akal.

// Contoh format validation rules yang umum:

type RegisterInput struct {
    Email     string `json:"email"     validate:"required,email"`
    Phone     string `json:"phone"     validate:"required,e164"`     // +628xxx
    BirthDate string `json:"birthDate" validate:"required,datetime=2006-01-02"`
    ProductID string `json:"productId" validate:"required,uuid4"`
    Website   string `json:"website"   validate:"omitempty,url"`
}
Format yang perlu divalidasi dengan hati-hati:

Email:
  ✓ Cek format dasar (ada @, ada domain)
  ✗ Jangan gunakan regex super kompleks — false positive banyak
  ✗ Jangan cek apakah email "benar-benar valid" di sini — itu tugas verifikasi email

Phone:
  ✓ E.164 format: +628123456789
  ✓ Atau format lokal yang disepakati
  ✗ Jangan hardcode regex country-specific — tidak scalable

UUID:
  ✓ UUID v4 format: 8-4-4-4-12 hex dengan versi byte yang benar
  ✗ Jangan hanya cek "apakah panjangnya 36 karakter"

Date/Time:
  ✓ ISO 8601: 2024-01-27 atau 2024-01-27T14:32:00Z
  ✗ Jangan terima format yang ambigu (01/02/03 — bulan/hari/tahun mana?)

Layer 3 — Length dan Size Validation #

Size validation melindungi sistem dari data yang terlalu besar — baik yang disengaja (abuse) maupun tidak (user paste artikel panjang ke field nama).

Field yang wajib punya size limit:

String fields:
  Nama user:        max 100 karakter
  Deskripsi produk: max 2000 karakter
  Judul:            max 200 karakter
  Pesan/komentar:   max 5000 karakter
  Password:         min 8, max 128 karakter (max password penting — bcrypt mahal untuk string panjang)

Array fields:
  Item dalam cart:    max 100 items
  Tags:               max 20 tags
  Batch insert:       max 1000 items

Request body:
  Ukuran total body: sesuaikan dengan kebutuhan, default 1–10 MB

File upload:
  Per file: sesuai kebutuhan (foto profil: 5 MB, dokumen: 20 MB)
  Total per request: batas yang wajar
// ANTI-PATTERN: Tidak ada size limit pada array
func BulkCreateUsers(users []User) {
    // users bisa berisi 1 juta item
    // Ini akan menghabiskan memory dan crash server
    for _, u := range users {
        db.CreateUser(u)
    }
}

// BENAR: Limit yang eksplisit
func BulkCreateUsers(users []User) error {
    if len(users) == 0 {
        return ErrEmptyInput
    }
    if len(users) > 500 {
        return ErrBatchTooLarge  // "Maksimal 500 item per request"
    }
    // ...
}
Batas panjang password di 128 karakter adalah best practice yang sering mengejutkan. Tanpa batas, attacker bisa mengirim password 1 juta karakter yang akan membuat bcrypt bekerja sangat lama (bcrypt bersifat O(n)), menyebabkan CPU spike dan DoS. Batas 128 karakter lebih dari cukup untuk password yang aman.

Layer 4 — Value Constraint Validation #

Value constraint memastikan nilai berada dalam rentang atau himpunan yang diizinkan oleh sistem.

// Enum validation — hanya nilai yang sudah didefinisikan yang diterima
type OrderStatus string

const (
    OrderStatusPending   OrderStatus = "pending"
    OrderStatusConfirmed OrderStatus = "confirmed"
    OrderStatusShipped   OrderStatus = "shipped"
    OrderStatusDelivered OrderStatus = "delivered"
    OrderStatusCancelled OrderStatus = "cancelled"
)

func (s OrderStatus) IsValid() bool {
    switch s {
    case OrderStatusPending, OrderStatusConfirmed,
         OrderStatusShipped, OrderStatusDelivered, OrderStatusCancelled:
        return true
    }
    return false
}

// Range validation
type CreateProductInput struct {
    Price    float64 `validate:"required,min=0.01,max=999999999"`
    Quantity int     `validate:"required,min=0,max=100000"`
    Discount float64 `validate:"min=0,max=100"`  // persentase: 0-100
}
Contoh value constraints yang sering diperlukan:
  Harga:      min=0.01 (tidak boleh nol atau negatif)
  Persentase: min=0, max=100
  Rating:     min=1, max=5, harus integer
  Tahun:      min=1900, max=2100
  Latitude:   min=-90, max=90
  Longitude:  min=-180, max=180
  Status:     enum yang terdefinisi (bukan free-form string)

Layer 5 — Business Validation #

Business validation adalah validation yang tidak bisa diselesaikan hanya dengan melihat data input — ia perlu konteks dari domain dan state sistem saat ini.

Karakteristik business validation:
  → Membutuhkan query ke database atau service lain
  → Bergantung pada state yang ada (saldo, stok, jadwal)
  → Mencerminkan aturan domain bisnis
  → Tidak bisa direpresentasikan sebagai rule stateless

Contoh:
  ✓ "Email tidak boleh sudah terdaftar" → cek di DB
  ✓ "Saldo tidak boleh melebihi total transfer" → cek balance user
  ✓ "Stok harus tersedia" → cek inventory
  ✓ "Jadwal tidak boleh overlap" → cek existing bookings
  ✓ "Coupon masih berlaku" → cek expiry dan usage count
// Contoh business validation terpisah dari business logic
type CreateOrderValidator struct {
    inventoryRepo InventoryRepository
    userRepo      UserRepository
}

func (v *CreateOrderValidator) Validate(input CreateOrderInput, userID string) error {
    // Validasi stok untuk setiap item
    for _, item := range input.Items {
        stock, err := v.inventoryRepo.GetStock(item.ProductID)
        if err != nil {
            return ErrInternal
        }
        if stock < item.Quantity {
            return &ValidationError{
                Field:   "items." + item.ProductID,
                Code:    "INSUFFICIENT_STOCK",
                Message: fmt.Sprintf("Stok tidak cukup untuk produk %s", item.ProductID),
            }
        }
    }

    // Validasi alamat pengiriman
    user, _ := v.userRepo.FindByID(userID)
    if !user.HasShippingAddress() {
        return &ValidationError{
            Field:   "address",
            Code:    "NO_SHIPPING_ADDRESS",
            Message: "Tambahkan alamat pengiriman terlebih dahulu",
        }
    }

    return nil
}

Layer 6 — Authorization Validation #

Authorization validation sering dianggap terpisah dari validation, tapi ia adalah salah satu layer yang paling kritis. Ini adalah pertanyaan: “Apakah user yang mengirim request ini berhak melakukan operasi ini terhadap resource ini?”

// ANTI-PATTERN: Tidak ada authorization validation
func UpdateOrder(r *http.Request) {
    orderId := r.PathValue("id")
    user := getAuthenticatedUser(r)

    order, _ := db.GetOrder(orderId)
    // Tidak cek apakah order milik user!
    order.Status = input.Status
    db.UpdateOrder(order)
}
// User A bisa update order milik User B

// BENAR: Authorization validation yang eksplisit
func UpdateOrder(r *http.Request) {
    orderId := r.PathValue("id")
    user := getAuthenticatedUser(r)

    // Authorization validation: pastikan order milik user ini
    order, err := db.GetOrderByIDAndUserID(orderId, user.ID)
    if err != nil || order == nil {
        // Return 404, bukan 403 — jangan konfirmasi bahwa resource ada
        respondError(w, 404, "NOT_FOUND", "Order tidak ditemukan")
        return
    }

    // Validasi state: apakah order bisa diupdate dalam status ini?
    if !order.CanBeUpdated() {
        respondError(w, 422, "INVALID_STATE", "Order tidak bisa diupdate dalam status " + order.Status)
        return
    }

    // Lanjutkan update
}

Layer 7 — Contextual Validation #

Contextual validation adalah validation yang bergantung pada kombinasi field atau state request lainnya. Field yang opsional dalam satu kondisi bisa menjadi wajib di kondisi lain.

// Contoh: reason wajib diisi jika status adalah "rejected"
type UpdateApplicationInput struct {
    Status string  `json:"status" validate:"required,oneof=approved rejected pending"`
    Reason *string `json:"reason"`  // opsional secara umum
}

func (i *UpdateApplicationInput) Validate() error {
    // Validasi kontekstual: reason wajib jika status rejected
    if i.Status == "rejected" && (i.Reason == nil || *i.Reason == "") {
        return &ValidationError{
            Field:   "reason",
            Code:    "REQUIRED_WHEN_REJECTED",
            Message: "Alasan penolakan wajib diisi",
        }
    }

    // Validasi kontekstual: reason tidak relevan jika status approved
    if i.Status == "approved" && i.Reason != nil {
        // Boleh diignore atau dikembalikan sebagai warning
        i.Reason = nil
    }

    return nil
}
Pola contextual validation yang umum:
  → Field A wajib jika Field B bernilai X
  → Field C tidak boleh ada jika status adalah Y
  → Tanggal akhir harus setelah tanggal awal
  → Jika payment method = "bank_transfer", bank_account wajib ada
  → Jika type = "recurring", frequency wajib diisi

Client-Side vs Server-Side Validation #

Ini adalah perbedaan yang sangat penting dan sering disalahpahami.

flowchart LR
    subgraph Client["Client Side"]
        CInput["User Input"]
        CV["Client Validation\n(JavaScript, HTML5)"]
        CInput --> CV
        CV -->|"Invalid"| CError["Error di UI\n(UX, tidak butuh round-trip)"]
    end

    subgraph Server["Server Side"]
        SInput["Request dari Client"]
        SV["Server Validation\n(Single Source of Truth)"]
        SInput --> SV
        SV -->|"Invalid"| SError["422 Response\n(Canonical, harus ada)"]
        SV -->|"Valid"| BL["Business Logic"]
    end

    CV -->|"Valid (via HTTP)"| SInput
Client-side validation:
  Tujuan: UX — memberikan feedback cepat tanpa round-trip ke server
  Kelebihan: Instan, tidak butuh network
  Kelemahan: Bisa dimatikan, bisa dimanipulasi, bisa di-bypass
  Status: TIDAK DAPAT DIANDALKAN untuk keamanan

Server-side validation:
  Tujuan: Keamanan, integritas data, konsistensi
  Kelebihan: Tidak bisa di-bypass, single source of truth
  Kelemahan: Butuh round-trip ke server
  Status: WAJIB ADA, tidak opsional

Kesimpulan:
  Gunakan keduanya — client-side untuk UX, server-side untuk keamanan.
  Jika harus memilih satu: server-side selalu lebih penting.

Format Error Response Validation #

Konsistensi format error sangat penting — frontend dan consumer API membutuhkan format yang predictable untuk menampilkan error dengan benar.

// Response HTTP 422 Unprocessable Entity untuk validation error

// Contoh 1: Single field error
{
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "Input tidak valid",
    "details": [
      {
        "field": "email",
        "code": "INVALID_FORMAT",
        "message": "Format email tidak valid"
      }
    ]
  }
}

// Contoh 2: Multiple field errors (kumpulkan semua, bukan stop di error pertama)
{
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "Input tidak valid",
    "details": [
      {
        "field": "email",
        "code": "REQUIRED",
        "message": "Email wajib diisi"
      },
      {
        "field": "phone",
        "code": "INVALID_FORMAT",
        "message": "Format nomor telepon tidak valid"
      },
      {
        "field": "birthDate",
        "code": "UNDERAGE",
        "message": "Usia minimal 17 tahun untuk mendaftar"
      }
    ]
  }
}

// Contoh 3: Nested field error
{
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "Input tidak valid",
    "details": [
      {
        "field": "items[0].quantity",
        "code": "MIN_VALUE",
        "message": "Jumlah minimal adalah 1"
      },
      {
        "field": "items[2].productId",
        "code": "NOT_FOUND",
        "message": "Produk tidak ditemukan"
      }
    ]
  }
}

Kumpulkan semua error sebelum mengembalikan response — jangan stop di error pertama. User harus bisa memperbaiki semua masalah sekaligus, bukan satu per satu setelah submit berulang.


Memisahkan Validation dari Business Logic #

Salah satu anti-pattern paling umum adalah validation yang bercampur aduk dengan business logic dalam satu fungsi. Ini membuat kode sulit dites, sulit dibaca, dan sulit direuse.

// ANTI-PATTERN: Validation campur dengan business logic
func (s *OrderService) CreateOrder(input CreateOrderInput, userID string) (*Order, error) {
    // Validation
    if input.Address == "" {
        return nil, errors.New("address required")
    }
    if len(input.Items) == 0 {
        return nil, errors.New("items required")
    }
    // ... 50 baris validasi lagi ...

    // Business logic
    order := &Order{
        UserID: userID,
        // ...
    }

    // Database operation
    return s.repo.Create(order)
}
// Susah di-test, tidak bisa reuse validator di tempat lain

// BENAR: Validator terpisah
type CreateOrderValidator struct{ /* dependencies */ }

func (v *CreateOrderValidator) Validate(input CreateOrderInput, userID string) []ValidationError {
    var errors []ValidationError
    if input.Address == "" {
        errors = append(errors, ValidationError{Field: "address", Code: "REQUIRED"})
    }
    // ... validation rules ...
    return errors
}

// Service menggunakan validator secara terpisah
func (s *OrderService) CreateOrder(input CreateOrderInput, userID string) (*Order, error) {
    if errs := s.validator.Validate(input, userID); len(errs) > 0 {
        return nil, &ValidationErrors{Errors: errs}
    }
    // Business logic murni, tidak ada validation di sini
    // ...
}

Manfaat pemisahan ini: validator bisa dites secara unit test tanpa menjalankan business logic, bisa direuse di tempat lain, dan lebih mudah di-maintain ketika aturan berubah.


Testing Validation yang Efektif #

Validation tanpa test adalah validation yang tidak dipercaya. Test untuk validation harus mencakup tidak hanya happy path tapi juga edge case dan negative scenario.

// Contoh test yang komprehensif untuk validator
func TestCreateOrderValidator(t *testing.T) {
    validator := NewCreateOrderValidator(mockInventory, mockUserRepo)

    tests := []struct {
        name        string
        input       CreateOrderInput
        userID      string
        expectError bool
        errorCode   string
    }{
        // Happy path
        {
            name:        "valid order",
            input:       validOrderInput(),
            userID:      "usr_123",
            expectError: false,
        },
        // Structural validation
        {
            name:        "empty items",
            input:       CreateOrderInput{Items: []OrderItem{}},
            userID:      "usr_123",
            expectError: true,
            errorCode:   "ITEMS_REQUIRED",
        },
        // Business validation
        {
            name: "insufficient stock",
            input: CreateOrderInput{
                Items: []OrderItem{{ProductID: "p1", Quantity: 1000}},
            },
            userID:      "usr_123",
            expectError: true,
            errorCode:   "INSUFFICIENT_STOCK",
        },
        // Authorization validation
        {
            name:        "user without shipping address",
            input:       validOrderInput(),
            userID:      "usr_no_address",
            expectError: true,
            errorCode:   "NO_SHIPPING_ADDRESS",
        },
        // Edge case
        {
            name: "quantity at max limit",
            input: CreateOrderInput{
                Items: []OrderItem{{ProductID: "p1", Quantity: 100}},
            },
            userID:      "usr_123",
            expectError: false,
        },
        {
            name: "quantity exceeds max limit",
            input: CreateOrderInput{
                Items: []OrderItem{{ProductID: "p1", Quantity: 101}},
            },
            userID:      "usr_123",
            expectError: true,
            errorCode:   "QUANTITY_EXCEEDS_LIMIT",
        },
    }

    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            errs := validator.Validate(tt.input, tt.userID)
            if tt.expectError && len(errs) == 0 {
                t.Errorf("expected error %s but got none", tt.errorCode)
            }
            if !tt.expectError && len(errs) > 0 {
                t.Errorf("expected no error but got: %v", errs)
            }
        })
    }
}
Satu test case per validation rule adalah pola yang baik untuk memulai. Tapi jangan lupa juga test kombinasi yang bisa menghasilkan behavior tak terduga — misalnya, apa yang terjadi jika field yang saling bergantung keduanya kosong, atau keduanya terisi dengan nilai konflik.

Anti-Pattern Validation yang Harus Dihindari #

Validasi Hanya di Client-Side #

// ✗ Anti-pattern: Percaya validasi JavaScript
"Kita sudah validasi di frontend, tidak perlu di backend"
→ Developer mode di Chrome: network request bisa dikirim langsung
→ Postman atau curl: client-side validation tidak berlaku
→ XSS attack bisa inject form submission yang bypass validation

// ✓ Solusi: Server-side validation selalu ada, client-side adalah tambahan UX

Stop di Error Pertama #

// ✗ Anti-pattern: Return satu error, paksa user submit berkali-kali
POST /register { email: "invalid", phone: "", birthDate: "bukan tanggal" }
Response: { "error": "Email tidak valid" }
User perbaiki email, submit lagi...
Response: { "error": "Phone wajib diisi" }
User perbaiki phone, submit lagi...
Response: { "error": "Tanggal lahir tidak valid" }
→ User harus submit 3 kali untuk tahu semua yang salah

// ✓ Solusi: Kumpulkan semua error, return sekaligus
Response: {
  "error": {
    "code": "VALIDATION_ERROR",
    "details": [
      { "field": "email", "code": "INVALID_FORMAT" },
      { "field": "phone", "code": "REQUIRED" },
      { "field": "birthDate", "code": "INVALID_FORMAT" }
    ]
  }
}

Validation Logic Tersebar di Mana-Mana #

// ✗ Anti-pattern: Rules tersebar di controller, service, model
// Di controller:
if input.Email == "" { return error }

// Di service:
if input.Email == "" { return error }  // duplikat!
// Juga ada cek lain di service

// Di model:
// Ada juga validation di model

// Perubahan rule = perlu update di 3 tempat
// Mudah lupa update salah satu → inkonsistensi

// ✓ Solusi: Satu tempat untuk validation logic
type RegisterValidator struct{}
func (v *RegisterValidator) Validate(input RegisterInput) []ValidationError { /* semua rules di sini */ }

Pesan Error yang Mengekspos Detail Internal #

// ✗ Anti-pattern: Error yang bocorkan info sensitif
{
  "error": "UNIQUE constraint failed: users.email"
}
// Attacker tahu: database engine, nama tabel, nama kolom

// ✓ Solusi: Error yang informatif tapi aman
{
  "error": {
    "code": "EMAIL_ALREADY_REGISTERED",
    "message": "Email ini sudah terdaftar. Gunakan fitur forgot password jika lupa sandi."
  }
}

Checklist Validation #

STRUCTURAL VALIDATION:
  □ Semua field wajib diperiksa keberadaannya
  □ Tipe data semua field divalidasi
  □ Unknown fields di-reject (DisallowUnknownFields atau equivalent)
  □ JSON malformed ditangani dengan graceful error

FORMAT VALIDATION:
  □ Email divalidasi dengan library, bukan regex custom
  □ Phone number divalidasi terhadap format yang disepakati
  □ UUID divalidasi format yang benar
  □ Date/time hanya menerima format standar (ISO 8601)
  □ URL divalidasi jika ada field URL

SIZE VALIDATION:
  □ Semua string field punya max length yang eksplisit
  □ Array field punya max count yang eksplisit
  □ Request body size di-limit di level server
  □ Password max length ada (untuk mencegah bcrypt DoS)

VALUE CONSTRAINT:
  □ Enum field hanya menerima nilai yang terdefinisi
  □ Angka punya min/max yang sesuai domain
  □ Persentase, rating, koordinat punya range yang tepat

BUSINESS VALIDATION:
  □ Uniqueness constraint diperiksa sebelum insert (bukan rely pada DB error)
  □ State transition divalidasi (boleh transisi dari A ke B?)
  □ Resource yang direferensikan diperiksa keberadaannya

AUTHORIZATION VALIDATION:
  □ Ownership divalidasi — resource milik user yang request?
  □ Role/permission divalidasi sesuai operasi
  □ Status resource memungkinkan operasi yang diminta?

ERROR RESPONSE:
  □ Semua error dikumpulkan sebelum return (tidak stop di pertama)
  □ Format error konsisten di semua endpoint
  □ Field error menggunakan path yang eksplisit (items[0].quantity)
  □ Tidak ada detail database atau stack trace di error message
  □ Error code bersifat machine-readable (REQUIRED, INVALID_FORMAT, dll)

TESTING:
  □ Unit test untuk setiap validation rule
  □ Test edge case (nilai di batas limit, kombinasi field)
  □ Test negative scenario (input yang seharusnya ditolak)
  □ Integration test untuk business validation yang butuh DB

Ringkasan #

  • Validation adalah tujuh layer, bukan satu — structural, format, size, value constraint, business, authorization, dan contextual. Masing-masing melindungi aspek yang berbeda dan tidak bisa digantikan satu sama lain.
  • Client-side validation adalah UX, bukan security — bisa dimatikan, bisa di-bypass. Server-side validation adalah satu-satunya yang bisa diandalkan untuk keamanan dan integritas data.
  • Kumpulkan semua error sebelum return — jangan hentikan validasi di error pertama. User harus bisa memperbaiki semua masalah sekaligus.
  • Pisahkan validator dari business logic — validator yang mandiri bisa dites secara unit test, bisa direuse, dan lebih mudah di-maintain ketika aturan berubah.
  • DisallowUnknownFields adalah pertahanan pertama mass assignment — satu baris yang mencegah field tidak dikenal masuk ke sistem.
  • Format error harus konsisten dan machine-readable — gunakan code yang machine-readable (REQUIRED, INVALID_FORMAT) dan message yang human-readable. Frontend butuh keduanya.
  • Pesan error yang informatif tapi tidak bocorkan detail internal — “Email sudah terdaftar” informatif. “UNIQUE constraint failed: users.email” adalah information disclosure.
  • Authorization validation adalah bagian dari validation — memeriksa kepemilikan resource dan permission adalah validation, bukan hanya urusan middleware.
  • Size limit di semua field — string max length, array max count, body size limit. Tanpa ini, sistem rentan terhadap payload yang menghabiskan memory.
  • Test semua kombinasi yang mungkin buruk — validation test harus mencakup happy path, edge case, negative scenario, dan kombinasi field yang bisa menghasilkan behavior tak terduga.

← Sebelumnya: Setup   Berikutnya: DB Transaction →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact