Validasi request berlapis di Laravel adalah cara praktis untuk menahan abuse pada API sebelum data masuk lebih jauh ke aplikasi, database, atau proses bisnis. Tujuannya bukan hanya memastikan format input benar, tetapi juga membatasi bentuk payload, menolak field liar, mengendalikan ukuran data, dan memisahkan validasi teknis dari aturan domain.

Kalau API Anda menerima JSON dari klien publik atau integrasi pihak ketiga, validasi dasar seperti required dan string saja tidak cukup. Anda perlu beberapa lapisan: Form Request untuk pintu masuk utama, middleware untuk aturan yang berlaku global, validator manual untuk skenario dinamis, domain-level guard untuk aturan bisnis, serta rate limiting sebagai pengaman tambahan saat validasi masih harus memproses banyak request.

Mengapa validasi request berlapis diperlukan

Abuse pada API tidak selalu berupa serangan canggih. Sering kali bentuknya sederhana:

  • mengirim field yang tidak didukung untuk mencoba memengaruhi perilaku sistem,
  • payload terlalu besar agar membebani parsing dan validasi,
  • array bertingkat dengan jumlah item berlebihan,
  • nilai kondisional yang sengaja dibuat ambigu,
  • input yang lolos validasi format tetapi melanggar aturan bisnis.

Kalau semua ini hanya ditangani di controller atau bahkan setelah query database berjalan, API akan lebih mahal diproses dan lebih sulit diaudit. Dengan validasi berlapis, request ditolak sedini mungkin, dengan respons yang konsisten dan log yang berguna untuk investigasi.

Desain lapisan validasi di Laravel

1. Form Request sebagai lapisan utama

Form Request cocok untuk mayoritas endpoint API karena aturan validasi, otorisasi, sanitasi awal, dan respons gagal bisa dipusatkan dalam satu kelas. Ini menjaga controller tetap tipis dan konsisten.

Misalkan kita punya endpoint untuk membuat order:

POST /api/orders

Contoh payload yang ingin diterima:

{
  "customer_id": 123,
  "note": "Kirim sore",
  "items": [
    { "sku": "SKU-001", "qty": 2 },
    { "sku": "SKU-002", "qty": 1 }
  ],
  "metadata": {
    "source": "mobile-app"
  }
}

Contoh Form Request:

<?php

namespace App\Http\Requests\Api;

use Illuminate\Contracts\Validation\Validator;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Http\Exceptions\HttpResponseException;
use Illuminate\Support\Arr;
use Illuminate\Validation\Rule;

class StoreOrderRequest extends FormRequest
{
    public function authorize(): bool
    {
        return true;
    }

    protected function prepareForValidation(): void
    {
        $note = $this->input('note');

        $this->merge([
            'note' => is_string($note) ? trim($note) : $note,
            'metadata' => is_array($this->input('metadata')) ? $this->input('metadata') : [],
        ]);
    }

    public function rules(): array
    {
        return [
            'customer_id' => ['required', 'integer', 'min:1'],
            'note' => ['nullable', 'string', 'max:500'],

            'items' => ['required', 'array', 'min:1', 'max:50'],
            'items.*.sku' => ['required', 'string', 'max:64'],
            'items.*.qty' => ['required', 'integer', 'min:1', 'max:1000'],

            'metadata' => ['sometimes', 'array'],
            'metadata.source' => ['sometimes', 'string', Rule::in(['web', 'mobile-app', 'partner'])],
        ];
    }

    public function messages(): array
    {
        return [
            'items.max' => 'Jumlah item melebihi batas maksimum.',
            'items.*.qty.max' => 'Kuantitas item terlalu besar.',
        ];
    }

    public function withValidator($validator): void
    {
        $validator->after(function ($validator) {
            $allowedTopLevel = ['customer_id', 'note', 'items', 'metadata'];
            $unknownTopLevel = array_diff(array_keys($this->all()), $allowedTopLevel);

            if (!empty($unknownTopLevel)) {
                $validator->errors()->add('request', 'Terdapat parameter yang tidak dikenali: '.implode(', ', $unknownTopLevel));
            }

            foreach ((array) $this->input('items', []) as $index => $item) {
                if (!is_array($item)) {
                    continue;
                }

                $allowedItemKeys = ['sku', 'qty'];
                $unknownItemKeys = array_diff(array_keys($item), $allowedItemKeys);

                if (!empty($unknownItemKeys)) {
                    $validator->errors()->add("items.$index", 'Field item tidak dikenali: '.implode(', ', $unknownItemKeys));
                }
            }
        });
    }

    protected function failedValidation(Validator $validator): void
    {
        throw new HttpResponseException(response()->json([
            'message' => 'Validasi gagal.',
            'errors' => $validator->errors(),
        ], 422));
    }

    public function validatedPayload(): array
    {
        return Arr::only($this->validated(), [
            'customer_id', 'note', 'items', 'metadata'
        ]);
    }
}

Di controller, gunakan hanya data yang sudah tervalidasi:

public function store(StoreOrderRequest $request)
{
    $payload = $request->validatedPayload();

    // teruskan ke service/domain layer
    return response()->json([
        'message' => 'Order diterima.',
        'data' => $payload,
    ], 201);
}

Pola ini bekerja karena ada pemisahan yang jelas:

  • prepareForValidation() untuk sanitasi ringan,
  • rules() untuk bentuk dan batasan data,
  • withValidator() untuk validasi tambahan yang tidak enak ditulis sebagai rule sederhana,
  • validatedPayload() untuk memastikan hanya field yang sudah diizinkan yang dipakai lebih lanjut.

2. Rule bawaan Laravel yang paling relevan untuk hardening API

Untuk mencegah abuse, beberapa rule biasanya lebih berguna daripada yang terlihat:

  • array: memastikan struktur sesuai ekspektasi.
  • min dan max: bukan hanya untuk angka, tetapi juga panjang string, jumlah elemen array, dan ukuran batas logis.
  • distinct: mencegah item duplikat pada array tertentu.
  • in atau Rule::in(): membatasi nilai ke whitelist yang eksplisit.
  • nullable vs sometimes: penting untuk membedakan field yang boleh kosong dengan field yang boleh tidak dikirim sama sekali.
  • required_with, required_if, prohibited_unless, dan keluarga rule kondisional lain: membantu membatasi kombinasi input yang ambigu.

Contoh tambahan untuk payload yang memiliki mode operasi:

public function rules(): array
{
    return [
        'mode' => ['required', Rule::in(['pickup', 'delivery'])],
        'pickup_point_id' => ['nullable', 'integer', 'required_if:mode,pickup'],
        'delivery_address' => ['nullable', 'string', 'required_if:mode,delivery', 'max:1000'],
    ];
}

Validasi kondisional seperti ini mengurangi input yang saling bertentangan. Tanpa aturan semacam ini, API bisa menerima request yang “secara format valid” tetapi secara makna membingungkan.

3. Sanitasi input: perlu, tetapi jangan menyembunyikan data buruk

Sanitasi input berguna untuk normalisasi ringan, misalnya trim string, mengubah nilai kosong tertentu, atau memaksa default aman. Namun sanitasi bukan pengganti validasi.

Yang aman dilakukan sebelum validasi:

  • trim whitespace pada field string,
  • normalisasi array kosong,
  • menghapus karakter kontrol tertentu jika memang disyaratkan oleh kontrak API.

Yang perlu dihindari:

  • mengubah tipe data secara agresif sehingga input buruk tampak valid,
  • diam-diam membuang field penting tanpa jejak,
  • memperbaiki nilai bisnis yang seharusnya ditolak.

Prinsip praktis: sanitasi untuk normalisasi teknis, validasi untuk penegakan kontrak. Kalau suatu nilai sebenarnya tidak boleh diterima, lebih baik ditolak jelas daripada “dibersihkan” secara diam-diam.

Whitelist field dan penolakan parameter tak dikenal

Salah satu celah yang sering diabaikan adalah menerima parameter tambahan tanpa efek yang jelas. Ini berbahaya karena:

  • klien bisa mengira field tersebut didukung padahal diabaikan,
  • di masa depan field baru bisa bentrok dengan input lama yang sudah beredar,
  • penyerang bisa mencoba banyak variasi field untuk memetakan perilaku sistem.

Laravel memudahkan validasi field yang ada, tetapi penolakan field yang tidak dikenal sering perlu ditulis eksplisit. Pendekatan yang aman:

  1. definisikan daftar field yang diizinkan di level top-level,
  2. lakukan hal yang sama untuk objek atau array bertingkat,
  3. setelah lolos validasi, tetap gunakan hanya hasil validated() atau whitelist final.

Contoh helper sederhana bila pola ini sering dipakai:

protected function unknownKeys(array $input, array $allowed): array
{
    return array_values(array_diff(array_keys($input), $allowed));
}

Trade-off pendekatan ketat ini adalah usability. Beberapa integrator terbiasa mengirim field ekstra untuk kompatibilitas internal mereka. Kalau API Anda publik dan ingin kontrak yang ketat, penolakan field tak dikenal layak diterapkan. Kalau API Anda dipakai internal dan perubahan kontrak sering, Anda bisa memilih hanya ignore but log selama masa transisi.

Membatasi payload dan array bertingkat

Abuse sering terjadi lewat payload yang “valid secara schema” tetapi terlalu besar. Contohnya, items berisi ribuan elemen atau setiap elemen membawa string sangat panjang. Validasi harus menetapkan batas yang masuk akal.

Batasi jumlah item

'items' => ['required', 'array', 'min:1', 'max:50'],
'items.*.sku' => ['required', 'string', 'max:64'],
'items.*.qty' => ['required', 'integer', 'min:1', 'max:1000'],

Ini penting karena validasi array bertingkat punya biaya. Semakin besar payload, semakin banyak rule yang diproses.

Batasi kedalaman dan bentuk struktur

Kalau API menerima objek bertingkat seperti metadata, tentukan field mana yang benar-benar didukung. Hindari menerima objek bebas kecuali memang perlu.

'metadata' => ['sometimes', 'array'],
'metadata.source' => ['sometimes', 'string', Rule::in(['web', 'mobile-app', 'partner'])],

Daripada membiarkan metadata berisi struktur arbitrer, lebih aman membatasi beberapa subfield yang diketahui. Jika memang harus menerima metadata bebas, pertimbangkan batas ukuran request di level server atau proxy, dan simpan dengan hati-hati agar tidak memicu ledakan ukuran log atau query.

Batas ukuran request di luar validator

Validator Laravel bekerja setelah request mencapai aplikasi. Untuk payload yang benar-benar besar, sebaiknya ada lapisan tambahan di web server, reverse proxy, atau infrastruktur API gateway agar request ditolak lebih awal. Ini penting karena parser request dan PHP tetap mengeluarkan biaya sebelum rule Laravel dijalankan.

Karena konfigurasi ini bergantung pada stack Anda, prinsip umumnya adalah:

  • batasi ukuran body request di edge,
  • batasi jumlah item dan panjang field di aplikasi,
  • hindari endpoint yang menerima JSON bebas tanpa batas eksplisit.

Kapan memakai middleware, validator manual, dan domain-level guard

Middleware: untuk aturan lintas endpoint

Middleware cocok untuk kebijakan umum yang tidak spesifik pada satu payload, misalnya:

  • menolak request tanpa Content-Type yang benar untuk endpoint JSON,
  • memastikan hanya metode HTTP tertentu yang dipakai dalam pola rute tertentu,
  • mencatat pola request mencurigakan sebelum masuk controller,
  • menerapkan rate limiting.

Middleware bukan tempat ideal untuk seluruh validasi field, karena aturan payload biasanya terlalu spesifik dan akan sulit dirawat jika dicampur di sana.

Validator manual: untuk skenario dinamis

Gunakan validator manual ketika aturan bergantung pada konteks runtime yang tidak nyaman ditaruh seluruhnya di Form Request, misalnya validasi batch dengan struktur berbeda tergantung jenis resource, atau ketika service layer perlu memvalidasi data yang tidak selalu berasal dari HTTP request.

use Illuminate\Support\Facades\Validator;
use Illuminate\Validation\Rule;

$validator = Validator::make($payload, [
    'type' => ['required', Rule::in(['sync', 'async'])],
    'callback_url' => ['nullable', 'url', 'required_if:type,async'],
]);

if ($validator->fails()) {
    return response()->json([
        'message' => 'Validasi gagal.',
        'errors' => $validator->errors(),
    ], 422);
}

Kelebihannya fleksibel. Kekurangannya, kalau terlalu sering dipakai di controller, struktur kode bisa kembali berantakan. Biasanya Form Request tetap menjadi default, validator manual dipakai saat memang perlu.

Domain-level guard: aturan bisnis yang tidak boleh hanya bergantung pada HTTP validation

Domain-level guard adalah lapisan yang memeriksa aturan bisnis inti setelah input lolos validasi teknis. Contohnya:

  • jumlah item order tidak boleh melewati limit paket pelanggan tertentu,
  • SKU yang dikirim harus termasuk katalog aktif,
  • request tidak boleh membuat kombinasi data yang dilarang oleh kebijakan internal.

Aturan ini tidak sebaiknya hanya ada di Form Request karena:

  • bisa dipanggil juga dari job, command, atau integrasi lain selain HTTP,
  • sering membutuhkan akses ke domain model atau repository,
  • lebih tepat dinyatakan sebagai aturan bisnis daripada format request.

Contoh sederhana di service layer:

class OrderService
{
    public function create(array $payload): array
    {
        if (count($payload['items']) > 50) {
            throw new DomainException('Jumlah item melebihi batas bisnis.');
        }

        // aturan domain lain...
        return $payload;
    }
}

Lapisan ini penting karena validasi HTTP bukan satu-satunya jalur masuk data ke sistem.

Respons error yang konsisten untuk API

Klien API lebih mudah diintegrasikan jika format error stabil. Untuk validasi, gunakan struktur yang mudah diparsing dan tidak berubah-ubah antare endpoint.

Contoh respons 422:

{
  "message": "Validasi gagal.",
  "errors": {
    "customer_id": ["Field customer_id wajib diisi."],
    "items.0.qty": ["Kuantitas item terlalu besar."],
    "request": ["Terdapat parameter yang tidak dikenali: debug"]
  }
}

Prinsipnya:

  • message ringkas untuk konteks umum,
  • errors berisi daftar per field atau per jalur atribut,
  • jangan membocorkan detail internal seperti query, stack trace, atau aturan backend yang sensitif.

Kalau aplikasi Anda memerlukan format error global yang seragam, sentralisasikan di exception handler agar 422 dari berbagai sumber tetap konsisten.

Logging pelanggaran dan sinyal abuse

Tidak semua validasi gagal adalah serangan. Namun pola tertentu layak dicatat, misalnya:

  • parameter tak dikenal berulang,
  • payload terlalu besar atau item terlalu banyak,
  • percobaan nilai di luar whitelist secara terus-menerus,
  • tingkat kegagalan tinggi dari IP atau client yang sama.

Contoh logging ringan di Form Request atau middleware:

use Illuminate\Support\Facades\Log;

Log::warning('API validation violation', [
    'path' => $this->path(),
    'ip' => $this->ip(),
    'keys' => array_keys($this->all()),
]);

Hati-hati saat logging:

  • jangan log seluruh payload mentah bila bisa mengandung data sensitif atau sangat besar,
  • lebih aman log ringkasan seperti path, IP, daftar key, jumlah item, dan jenis pelanggaran,
  • pastikan log tidak menjadi titik beban baru saat serangan berlangsung.

Rate limiting sebagai lapisan tambahan

Rate limiting tidak menggantikan validasi request, tetapi sangat efektif sebagai lapisan tambahan. Validasi tetap menghabiskan CPU dan memori; rate limiting membantu membatasi seberapa sering aktor yang sama bisa memaksa aplikasi memproses request buruk.

Di Laravel, Anda bisa menerapkan rate limiter pada rute API dan menyesuaikannya berdasarkan karakteristik endpoint. Endpoint yang menerima payload kompleks biasanya layak diberi limit lebih ketat daripada endpoint baca yang ringan.

Contoh prinsip penerapan:

  • beri limit berbeda untuk endpoint write dan read,
  • kombinasikan identitas client yang tersedia dengan IP sebagai dasar limit,
  • respons rate limit sebaiknya jelas dan konsisten,
  • monitor endpoint mana yang paling sering terkena limit karena itu sering menjadi sinyal abuse atau desain API yang kurang efisien.

Trade-off-nya: limit yang terlalu ketat bisa mengganggu klien sah, terutama integrasi batch. Karena itu, sesuaikan dengan pola trafik nyata dan evaluasi dari log.

Kesalahan umum yang sering terjadi

  • Memakai $request->all() setelah validasi. Ini membuka peluang field tak tervalidasi ikut diproses. Gunakan validated() atau whitelist eksplisit.
  • Terlalu longgar pada array bertingkat. Menerima array tanpa membatasi isi dan jumlah elemennya sering menjadi sumber abuse.
  • Sanitasi berlebihan. Mengubah input buruk menjadi tampak valid membuat debugging dan audit lebih sulit.
  • Mencampur validasi teknis dan aturan bisnis dalam controller. Hasilnya sulit diuji dan sulit dipakai ulang.
  • Tidak menolak field tak dikenal. Ini membuat kontrak API kabur dan rawan masalah kompatibilitas.
  • Tidak ada batas payload di edge. Aplikasi tetap menanggung biaya request besar sebelum validator bekerja.

Checklist implementasi Laravel untuk cegah abuse pada API

  1. Gunakan Form Request untuk setiap endpoint write yang menerima payload nontrivial.
  2. Tambahkan prepareForValidation() hanya untuk normalisasi aman seperti trim.
  3. Terapkan rule min/max pada string, angka, dan jumlah elemen array.
  4. Batasi enum dengan Rule::in() atau rule whitelist setara.
  5. Validasi array bertingkat secara eksplisit dengan pola items.*.field.
  6. Tolak atau minimal log parameter tak dikenal di top-level dan nested object penting.
  7. Gunakan hanya validated() atau metode whitelist final saat meneruskan data ke service.
  8. Pisahkan aturan bisnis ke domain/service layer agar tidak hanya bergantung pada HTTP.
  9. Standarkan respons 422 validation error agar mudah diparsing klien.
  10. Log pelanggaran yang relevan tanpa menyimpan payload sensitif atau terlalu besar.
  11. Tambahkan rate limiting pada endpoint yang rawan abuse.
  12. Jika memungkinkan, batasi ukuran request juga di level server, proxy, atau gateway.

Penutup

Laravel: validasi request berlapis untuk cegah abuse pada API bukan sekadar menambah banyak rule, tetapi menyusun pertahanan yang tepat di tempat yang tepat. Form Request menangani kontrak input, middleware menegakkan kebijakan umum, validator manual dipakai untuk kasus dinamis, domain-level guard menjaga aturan bisnis, dan rate limiting membatasi dampak trafik buruk.

Target akhirnya adalah keseimbangan: API cukup ketat untuk menolak request bermasalah sedini mungkin, tetapi tetap cukup jelas dan konsisten agar klien sah mudah berintegrasi. Jika Anda mulai dari satu hal hari ini, mulailah dengan whitelist field, batas array, dan penggunaan eksklusif validated payload. Tiga langkah itu sudah menutup banyak celah umum pada API Laravel.