Pendahuluan — kenapa batasi request setelah Feature Test

Artikel ini adalah #69 (ini) di Seri 5: Laravel Lanjutan. Setelah bentuk jawaban JSON pinjam dikunci lewat Feature Test API (#68), pertanyaan berikutnya muncul: bagaimana kalau ada yang memanggil API berulang-ulang sampai server kewalahan?

Tanpa batas, satu skrip bisa menekan tombol pinjam ratusan kali per menit — antrean di loket perpustakaan jadi kacau. Hari ini kita belajar rate limiting: petugas loket yang menahan antrean — maksimal N request per menit; kalau lebih, respons 429 dengan pesan “coba lagi nanti”.

Awam: bayangkan petugas loket perpustakaan yang berkata: “Maksimal lima orang per menit.” Yang datang ke-enam harus menunggu. Itu peran throttle dan RateLimiter setelah Feature Test memastikan JSON tetap benar.

Prasyarat: sudah selesai Feature Test API (#68), paham fondasi Instal PHP, Composer & Proyek Laravel (#56) / Struktur Folder, .env & Artisan Laravel (#57). Pakai Laravel 13+ — butuh PHP 8.3+.

Spesifikasi fitur — apa yang selesai hari ini?

Tiga hal ini yang kita kejar:

  1. Hitung request dalam jendela waktu — misalnya maksimal 5 panggilan per menit per IP.
  2. Tolak yang melewati batas — respons HTTP 429 Too Many Requests dengan pesan “coba lagi nanti”.
  3. Loloskan yang masih di bawah batas — respons normal 200 seperti biasa.

Awam: selesai artikel ini, kamu punya pola batas yang melindungi API pinjam dari spam — bukan mengganti validasi atau otorisasi, melainkan menahan banjir panggilan berulang. Fokus kita RateLimiter bawaan Laravel dengan middleware throttle. PHP murni dulu supaya logika hitung-cek-tolak terlihat jelas.

Istilah — ringkas untuk rate limiting

Istilah Arti awam Catatan
limit Batas maksimal — misalnya 5 kali per menit Angka yang tidak boleh dilampaui
throttle Middleware yang memperlambat/memblokir request berlebihan Seperti petugas loket yang menahan antrean
RateLimiter Kelas Laravel untuk mendefinisikan aturan batas Didaftarkan di AppServiceProvider
429 Status HTTP “terlalu banyak request” Artinya: coba lagi nanti, jangan spam
Jendela waktu Rentang hitungan — misalnya 60 detik terakhir Request lama di luar jendela tidak dihitung
Per IP Batas dihitung per alamat pengirim Satu pengguna spam tidak menghabiskan kuota semua orang

Urutan belajar kita: fungsi hitung PHP dulu -> demo pass/fail -> baru cuplikan RateLimiter Laravel. Kalau loncat langsung ke middleware tanpa paham apa yang dihitung, batas sering ditulis asal-asalan.

Persiapan — alat yang kamu buka

Alat yang dipakai di artikel ini (fondasi dari Instal PHP, Composer & Proyek Laravel (#56) dan Struktur Folder, .env & Artisan Laravel (#57) — tidak ada unduhan Composer baru hari ini):

  • Explorer — cek folder proyek perpustakaan-api, lalu lihat routes\api.php atau bootstrap\app.php untuk rute pinjam, serta app\Providers\AppServiceProvider.php untuk mendaftarkan RateLimiter.
  • Terminal — Laragon: menu Terminal · XAMPP: tombol Shell. Hindari CMD/PowerShell dari Start Menu kalau PATH PHP-mu belum rapi.
  • Editor teks — Notepad / VS Code — untuk membuka AppServiceProvider atau file rute. Contoh: notepad app\Providers\AppServiceProvider.php.
  • Browser — opsional. Inti uji hari ini ada di terminal; browser berguna kalau kamu sudah menjalankan php artisan serve dan ingin bandingkan dengan curl.exe.

Awam: untuk artikel ini satu terminal sebenarnya cukup — jalankan php laravel_rate_limiting_api_demo.php di folder proyek. Kalau php artisan serve dari artikel sebelumnya masih hidup, pakai terminal kedua untuk demo PHP dan perintah curl.exe berulang saat ingin melihat respons 429 di rute pinjam. Kalau butuh jendela kedua: Laragon — klik menu Terminal lagi · XAMPP — klik tombol Shell lagi, lalu cd ke folder proyek yang sama.

Buka terminal Laragon/Shell XAMPP, masuk ke folder proyek:

cd C:\laragon\www\perpustakaan-api

Di XAMPP biasanya: cd C:\xampp\htdocs\perpustakaan-api. Sesuaikan kalau foldermu beda.

Install-dari-nol: kalau php atau composer belum dikenali terminal, kembali dulu ke Instal PHP, Composer & Proyek Laravel (#56). Kalau struktur folder proyek masih membingungkan, ulangi Struktur Folder, .env & Artisan Laravel (#57).

Kenapa PHP biasa dulu?

Kalau langsung loncat ke middleware throttle di Laravel, pemula sering bingung: apa yang sebenarnya dihitung? Maka kita mulai dari fungsi PHP biasa yang menghitung request dalam jendela waktu — supaya perbedaan lolos vs ditolak terlihat jelas sebelum dibungkus RateLimiter.

<?php
// Mini: hitung request dalam jendela 60 detik, batas 3.
$limit = 3;
$window = 60;
$now = time();

$diBawahBatas = [$now - 10, $now - 20];
$diAtasBatas = [$now - 1, $now - 2, $now - 3, $now - 4];

function bolehLewat(array $waktuHit, int $limit, int $window, int $now): bool
{
    $masihAktif = array_filter($waktuHit, fn (int $t) => ($now - $t) < $window);
    return count($masihAktif) < $limit;
}

echo bolehLewat($diBawahBatas, $limit, $window, $now) ? "LOLOS" : "GAGAL", PHP_EOL;
echo bolehLewat($diAtasBatas, $limit, $window, $now) ? "LOLOS" : "GAGAL", PHP_EOL;

Awam — cara menguji bagian ini: salin potongan di atas ke file misalnya batas-cek.php, lalu di terminal Laragon/XAMPP jalankan php batas-cek.php. Kalau muncul LOLOS lalu GAGAL, ide “hitung dalam jendela waktu” sudah terlihat — yang di bawah batas lolos, yang melewati batas ditolak.

Alur batas — hitung, cek, tolak

Gerakan yang benar selalu sama:

  1. Hitung — berapa request masuk dalam jendela waktu (misalnya 60 detik terakhir).
  2. Cek — bandingkan dengan limit yang kamu tetapkan.
  3. Tolak — kalau sudah penuh, kembalikan 429 dengan pesan “coba lagi nanti”.
  4. Loloskan — kalau masih di bawah batas, lanjutkan ke logika pinjam biasa.
<?php
// Salin ke file misalnya batas-cek.php lalu jalankan: php batas-cek.php
$limit = 5;
$window = 60;
$now = time();

$riwayat = [$now - 5, $now - 10, $now - 15, $now - 20, $now - 25, $now - 30];

function hitungAktif(array $waktuHit, int $window, int $now): int
{
    return count(array_filter($waktuHit, fn (int $t) => ($now - $t) < $window));
}

function cekBatas(array $waktuHit, int $limit, int $window, int $now): array
{
    $aktif = hitungAktif($waktuHit, $window, $now);
    if ($aktif >= $limit) {
        return ["lolos" => false, "status" => 429, "pesan" => "coba lagi nanti"];
    }
    return ["lolos" => true, "status" => 200, "pesan" => "ok"];
}

$hasil = cekBatas($riwayat, $limit, $window, $now);
echo $hasil["lolos"] ? "CEK LOLOS" : "CEK GAGAL 429", PHP_EOL;
echo $hasil["status"], " — ", $hasil["pesan"], PHP_EOL;

Awam — cara menguji bagian ini: salin potongan di atas ke batas-cek.php, lalu di terminal jalankan php batas-cek.php. Kalau muncul CEK GAGAL 429 dan 429 — coba lagi nanti, fondasi tolak sudah sehat. Ini versi PHP murni dari apa yang nanti ditulis sebagai RateLimiter::for di Laravel.

Request masuk -> Hitung jendela -> Cek limit -> 200 atau 429 Masuk request API Hitung jendela 60s Cek limit Hasil 200 / 429 Petugas loket: maksimal N request per menit per IP. Setelah Feature Test mengunci JSON, rate limiting melindungi dari spam.
Setelah bentuk JSON dikunci di Feature Test API (#68), #69 (ini) melindungi API pinjam dari spam request berulang.

Laravel — cuplikan Rate Limiter (bukan file mandiri)

Di proyek Laravel, aturan batas didaftarkan di app\Providers\AppServiceProvider.php, lalu dipasang ke rute pinjam lewat middleware throttle.

<?php
// Cuplikan Laravel (bukan file mandiri)
// app/Providers/AppServiceProvider.php

use Illuminate\Cache\RateLimiting\Limit;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\RateLimiter;

public function boot(): void
{
    RateLimiter::for("pinjam", function (Request $request) {
        return Limit::perMinute(5)->by($request->ip());
    });
}

Pasang middleware ke rute pinjam di routes\api.php:

<?php
// Cuplikan Laravel (bukan file mandiri)
// routes/api.php

Route::middleware("throttle:pinjam")->post("/api/pinjam", [PeminjamanController::class, "store"]);

Awam: RateLimiter::for('pinjam', ...) = aturan “maksimal 5 per menit per IP”. throttle:pinjam = middleware yang menerapkan aturan itu sebelum logika pinjam dijalankan. Cuplikan ini bukan file mandiri — tempel ke proyek kalau rute pinjam sudah ada.

Uji dengan curl.exe berulang di terminal (kalau php artisan serve sudah jalan):

curl.exe -X POST "http://127.0.0.1:8000/api/pinjam" -H "Content-Type: application/json" -d "{\"buku_id\":1}"

Jalankan perintah di atas lebih dari 5 kali dalam satu menit — yang ke-enam harusnya mengembalikan 429.

Awam: curl.exe membantu melihat respons dengan mata — spam beberapa kali sampai muncul 429. Kalau muncul 404, rute pinjam mungkin belum dipasang — itu wajar; fokus dulu ke demo PHP di atas. Kalau php artisan serve belum jalan, cukup uji demo PHP; terminal kedua hanya untuk uji curl.exe berulang.

Pola Dasar — antrean yang terkendali

  1. 1
    Tetapkan limit
    Misalnya 5 request per menit per IP untuk rute pinjam.
  2. 2
    Daftarkan RateLimiter
    RateLimiter::for('pinjam', ...) di AppServiceProvider.
  3. 3
    Pasang middleware throttle
    throttle:pinjam pada rute POST pinjam di routes\api.php.
  4. 4
    Hitung dalam jendela
    Laravel menghitung otomatis — mirip fungsi hitungAktif di demo PHP.
  5. 5
    Tolak dengan 429
    Request ke-enam dalam menit yang sama mendapat 429 “coba lagi nanti”.
  6. 6
    Uji berulang
    curl.exe spam di terminal kedua — pastikan 429 muncul setelah batas tercapai.

Kode lengkap — demo mandiri

Simpan sebagai laravel_rate_limiting_api_demo.php, lalu jalankan php laravel_rate_limiting_api_demo.php:

<?php
declare(strict_types=1);

$limit = 3;
$window = 60;
$now = time();

$riwayatDiBawah = [$now - 10, $now - 20];
$riwayatDiAtas = [$now - 1, $now - 2, $now - 3, $now - 4];

function hitungAktif(array $waktuHit, int $window, int $now): int
{
    return count(array_filter($waktuHit, fn (int $t) => ($now - $t) < $window));
}

function cekRateLimit(array $waktuHit, int $limit, int $window, int $now): array
{
    $aktif = hitungAktif($waktuHit, $window, $now);
    if ($aktif >= $limit) {
        return ["lolos" => false, "status" => 429, "pesan" => "coba lagi nanti"];
    }
    return ["lolos" => true, "status" => 200, "pesan" => "ok"];
}

function demo(string $judul, array $waktuHit, int $limit, int $window, int $now): void
{
    echo "=== {$judul} ===", PHP_EOL;
    $hasil = cekRateLimit($waktuHit, $limit, $window, $now);
    echo $hasil["lolos"] ? "LOLOS" : "GAGAL", PHP_EOL;
    echo "HTTP ", $hasil["status"], " — ", $hasil["pesan"], PHP_EOL, PHP_EOL;
}

demo("Di bawah batas — harus lolos", $riwayatDiBawah, $limit, $window, $now);
demo("Di atas batas — harus gagal 429", $riwayatDiAtas, $limit, $window, $now);
demo("Tepat di batas — harus gagal 429", [$now - 1, $now - 2, $now - 3], $limit, $window, $now);

Awam — cara menguji bagian ini: simpan file sebagai laravel_rate_limiting_api_demo.php di folder proyek, lalu di terminal Laragon/XAMPP jalankan php laravel_rate_limiting_api_demo.php. Harusnya muncul satu LOLOS lalu dua GAGAL dengan HTTP 429 — coba lagi nanti. Fungsi cekRateLimit adalah inti logika; demo(...) hanya membungkus output agar mudah dibaca di terminal — mirip apa yang dilakukan RateLimiter di Laravel.

Kesalahan umum

Gejala Penyebab tipikal Perbaikan awam
Semua request ditolak, termasuk yang pertama limit diset 0 atau jendela terlalu sempit Cek angka di Limit::perMinute(5)
Spam tidak pernah kena 429 Middleware throttle belum dipasang ke rute Tambah throttle:pinjam di routes\api.php
Satu pengguna spam menghabiskan kuota semua orang Batas global, bukan per IP Pakai ->by($request->ip()) di RateLimiter
curl.exe selalu 404 Rute pinjam belum dipasang atau serve belum jalan Fokus demo PHP dulu; 404 wajar kalau rute belum ada
429 tidak muncul meski spam Cache driver tidak jalan di lokal Pastikan CACHE_STORE di .env bukan array untuk uji throttle
Batas terlalu ketat untuk penggunaan normal limit terlalu rendah untuk rute baca Pisahkan limit: ketat untuk POST pinjam, longgar untuk GET daftar

Latihan singkat

  1. Ubah demo: set $limit = 5 dan buat skenario yang lolos vs gagal.
  2. Jelaskan ke teman: beda petugas loket yang menahan antrean vs tidak ada batas sama sekali — pakai analogi perpustakaan.
  3. Tulis satu kalimat: kenapa 429 lebih baik daripada membiarkan server crash karena spam.

FAQ singkat

Apakah rate limiting menggantikan Feature Test?
Tidak. Feature Test dari Feature Test API (#68) mengunci bentuk JSON. Rate limiting melindungi API dari spam request berulang — tugas berbeda, keduanya saling melengkapi.

Haruskah batas sama untuk semua rute?
Tidak wajib. Rute POST pinjam biasanya lebih ketat (misalnya 5/menit) daripada GET daftar buku (misalnya 60/menit). Sesuaikan dengan beban nyata perpustakaan mini.

Tool apa yang dibuka dulu?
Explorer untuk memastikan folder proyek benar (routes\api.php + AppServiceProvider), satu terminal untuk demo PHP, editor untuk cuplikan Laravel. Kalau serve hidup, terminal kedua untuk curl.exe spam.

Potongan sintaks diuji di mana?
Langkah tengah (fungsi hitung jendela) salin ke batas-cek.php, lalu jalankan php batas-cek.php. Demo lengkap diuji dengan php laravel_rate_limiting_api_demo.php. Cuplikan Laravel ditempel ke app\Providers\AppServiceProvider.php dan routes\api.php; uji throttle dengan curl.exe berulang.

Ke mana setelah ini?
Berikutnya alami: Capstone: Pinjam & Kembalikan (#70) — satukan semua potongan ke alur pinjam–kembali utuh.

Kesimpulan

Kamu sudah melindungi API pinjam dari spam dengan rate limiting: fungsi hitung PHP dulu di batas-cek.php, demo pass/fail di laravel_rate_limiting_api_demo.php, lalu cuplikan RateLimiter dan middleware throttle Laravel. Petugas loket digital — maksimal N request per menit, sisanya 429 “coba lagi nanti”.

Seri 5 progress: langkah #69 (ini) · 6/7 Laravel Lanjutan · prasyarat: Feature Test API (#68) LIVE. Berikutnya: Capstone: Pinjam & Kembalikan (#70).