Input dari client API tidak boleh dipercaya begitu saja — payload kosong, field salah tipe, dan nilai di luar daftar pilihan adalah keseharian setiap endpoint publik. Di sisi web, validasi gagal membuat Laravel mengembalikan user ke form; di sisi API, tidak ada form untuk kembali — client membutuhkan jawaban mesin yang bisa dibaca program. Untungnya Laravel sudah menyiapkan format 422 yang rapi untuk itu. Chapter ini mempraktikkan validasi payload JSON dari yang paling sederhana sampai body bersarang dan query parameter, dengan output nyata di setiap langkah.

validate() Langsung di Controller #

Cara tercepat memvalidasi di API sama seperti di web: panggil $request->validate() di controller. Kita pakai store() dari API Resource Controllers yang sudah memvalidasi title, content, dan user_id. Endpoint ini menolak dua payload bermasalah berikut dengan status 422:

Output yang Dihasilkan:

text
### POST /api/v1/posts (validate inline) dengan body kosong → HTTP 422
{
    "message": "The title field is required. (and 2 more errors)",
    "errors": {
        "title": [
            "The title field is required."
        ],
        "content": [
            "The content field is required."
        ],
        "user_id": [
            "The user id field is required."
        ]
    }
}

### POST /api/v1/posts dengan body tidak lengkap → HTTP 422
{
    "message": "The content field is required. (and 1 more error)",
    "errors": {
        "content": [
            "The content field is required."
        ],
        "user_id": [
            "The selected user id is invalid."
        ]
    }
}

Anatomi Error 422 #

Format di atas layak dibedah karena client kita akan memprogram ulang bentuk ini terus-menerus:

  1. message — ringkasan satu kalimat untuk ditampilkan langsung ke pengguna akhir, misalnya di toast notification.
  2. errors — objek yang memetakan setiap field bermasalah ke array pesan. Array, karena satu field bisa melanggar beberapa rule sekaligus (misalnya required dan max:100).
  3. Status 422 Unprocessable Content — berbeda dari 400: request terformat benar, hanya isinya yang gagal lolos aturan bisnis.

Perhatikan pula bagaimana rule bekerja di payload kedua: content yang kosong ditolak required, sedangkan user_id bernilai 999 ditolak exists:users,id dengan pesan “The selected user id is invalid.” — setiap rule punya pesan khasnya sendiri.

FormRequest untuk Aturan yang Tumbuh #

Ketika aturan bertambah, memindahkannya ke FormRequest menjaga controller tetap ramping. Perintah artisan membuat kerangkanya:

bash
php artisan make:request StorePostRequest

Output yang Dihasilkan:

text
   INFO  Request [app/Http/Requests/StorePostRequest.php] created.

Kita isi rules() dan set authorize() mengembalikan true (autorisasi berbasis token kita bahas di chapter API Authorization ):

php
<?php

namespace App\Http\Requests;

use Illuminate\Contracts\Validation\ValidationRule;
use Illuminate\Foundation\Http\FormRequest;

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

    /**
     * @return array<string, ValidationRule|array<mixed>|string>
     */
    public function rules(): array
    {
        return [
            'title' => 'required|string|max:100',
            'content' => 'required|string|min:10',
            'user_id' => 'required|integer|exists:users,id',
            'status' => 'sometimes|in:draft,published',
            'tags' => 'sometimes|array|min:1',
            'tags.*' => 'string|max:30',
        ];
    }
}

Type-hint di controller menggantikan panggilan validate() — Laravel memvalidasi otomatis sebelum method berjalan:

php
<?php

// routes/api.php (di dalam group prefix v1) — contoh pemakaian

use App\Http\Requests\StorePostRequest;
use Illuminate\Support\Facades\Route;

Route::post('/posts-form-request', function (StorePostRequest $request) {
    return response()->json(['status' => 'valid', 'data' => $request->validated()], 201);
});

Payload yang sehat lolos dan validated() hanya mengembalikan field yang lolos validasi — input asing otomatis tersaring:

Output yang Dihasilkan:

text
### POST /api/v1/posts-form-request dengan data valid → HTTP 201
{
    "status": "valid",
    "data": {
        "title": "Post dari FormRequest",
        "content": "Konten yang cukup panjang.",
        "user_id": 1,
        "tags": [
            "laravel",
            "api"
        ]
    }
}

Validasi Body JSON Bersarang #

JSON memungkinkan struktur bersarang, dan rule validasi mengikutinya dengan notasi titik. Dua rule terakhir pada StorePostRequest menunjukkannya: tags wajib array bila hadir, dan tags.* memeriksa setiap elemennya. Payload dengan tag terlalu panjang ditolak dengan kunci error yang menunjuk posisi persisnya:

Output yang Dihasilkan:

text
### POST /api/v1/posts-form-request dengan tag terlalu panjang → HTTP 422
{
    "message": "The tags.1 field must not be greater than 30 characters.",
    "errors": {
        "tags.1": [
            "The tags.1 field must not be greater than 30 characters."
        ]
    }
}

Kunci tags.1 memberi tahu client bahwa elemen kedua array-lah yang bermasalah — cukup informasi untuk menyorot input yang salah di UI tanpa menebak.

Validasi Query Parameter #

Body bukan satu-satunya pintu masuk input: query parameter pada GET juga layak divalidasi sebelum dipakai memfilter data. Method validate() di route closure bekerja sama persis:

php
<?php

// routes/api.php (di dalam group prefix v1)

use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;

Route::get('/artikel', function (Request $request) {
    $request->validate([
        'status' => 'sometimes|in:draft,published',
        'page' => 'sometimes|integer|min:1',
    ]);

    return ['filter' => $request->only(['status', 'page'])];
});

Output yang Dihasilkan:

text
### GET /api/v1/artikel?status=archived (query param tidak valid) → HTTP 422
{
    "message": "The selected status is invalid.",
    "errors": {
        "status": [
            "The selected status is invalid."
        ]
    }
}

### GET /api/v1/artikel?status=published&page=2 (query param valid) → HTTP 200
{
    "filter": {
        "status": "published",
        "page": "2"
    }
}

Nilai query parameter selalu tiba sebagai string — "page": "2" pada output sukses — karena URL memang tidak mengenal tipe data; konversi ke integer tetap tanggung jawab kita saat memakainya.

expectsJson: 422 Bukan Selalu Otomatis #

Di sinilah jebakan yang paling sering menjebak pendatang baru API Laravel. Secara bawaan, validasi gagal dijawab JSON hanya bila request mengirim header Accept: application/json; tanpa header tersebut, Laravel menganggap pemanggil adalah browser dan menjawab redirect 302. Kita buktikan pada aplikasi tanpa konfigurasi tambahan:

Output yang Dihasilkan:

text
tanpa Accept json  → HTTP 302  Location=http://localhost  body=<!DOCTYPE html>...
dengan Accept json → HTTP 422  body={"message":"The title field is required. (and 2 more errors)","errors":{...}}

Validasi request kini teruji dari payload flat sampai query parameter, dengan format 422 yang ramah mesin. Tetapi satu pertanyaan mendasar belum terjawab: siapa yang boleh mengirim request itu? Masalah identitas di dunia stateless — token, Sanctum, dan kegagalan session — adalah topik chapter API Authentication . Daftar rule lengkap tersedia di dokumentasi validation .