Kita sudah menguasai seluruh teori: posisi lapisan, registrasi, parameter, dan empat titik pengolahan. Saatnya merakitnya menjadi tiga filter produksi yang sering dibutuhkan aplikasi nyata: membatasi akses berdasarkan alamat IP, memaksa seluruh error API tampil sebagai JSON, dan mencatat setiap request ke log. Ketiganya kita bangun penuh — class, config, route, dan feature test — lalu dirangkai bekerja sama pada satu route yang sama.
Filter 1: IP Whitelist #
Filter pertama membandingkan alamat IP pengirim dengan daftar yang tersimpan di config, sehingga daftar tersebut bisa berubah tanpa menyentuh kode:
// config/whitelist.php
return [
'ips' => [
'127.0.0.1',
'::1',
],
];
// app/Http/Middleware/IpWhitelist.php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
class IpWhitelist
{
public function handle(Request $request, Closure $next): Response
{
if (! in_array($request->ip(), config('whitelist.ips', []))) {
return response()->json([
'message' => 'Alamat IP Anda tidak diizinkan.',
], 403);
}
return $next($request);
}
}$request->ip() membaca alamat pengirim, in_array mencocokkannya dengan config, dan tamu yang tidak terdaftar menerima JSON 403 lewat short-circuit. Pada deployment nyata, perhatikan bahwa alamat yang dibaca adalah alamat langsung pengirim — bila aplikasi berada di belakang load balancer atau proxy, nilai itu adalah alamat proxy kecuali kita menyetel TrustProxies lewat $middleware->trustProxies(at: ...) agar header X-Forwarded-For dipercaya. Feature test kedua mengganti daftar config saat runtime untuk mensimulasikan IP asing:
public function test_ip_tidak_terdaftar_ditolak(): void
{
config(['whitelist.ips' => ['10.0.0.1']]);
$response = $this->get('/api/laporan/9');
$response->assertForbidden();
echo 'ip_asing: status=', $response->getStatusCode(),
', content-type=', $response->headers->get('Content-Type'), PHP_EOL;
}Output yang Dihasilkan:
ip_lokal: status=200, isi={"id":9,"judul":"Laporan Penjualan 2025"}
ip_asing: status=403, content-type=application/jsonFilter 2: Memaksa Response JSON #
Error bawaan Laravel untuk route web berbentuk halaman HTML — pas untuk browser, tidak untuk client API. Middleware berikut mengubah header Accept sebelum controller berjalan, sehingga exception handler menilai request ini sebagai permintaan JSON:
// app/Http/Middleware/ForceJsonResponse.php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
class ForceJsonResponse
{
public function handle(Request $request, Closure $next): Response
{
$request->headers->set('Accept', 'application/json');
return $next($request);
}
}Kedua route berikut sama-sama memanggil abort(404, 'Laporan tidak ditemukan.'); bedanya hanya pada middleware yang menempel. Test mematikan debug agar bentuk jawaban terlihat jernih:
public function test_error_api_tampil_sebagai_json(): void
{
config(['app.debug' => false]);
$response = $this->get('/api/laporan/99');
$response->assertNotFound();
echo 'laporan_hilang_json: status=', $response->getStatusCode(),
', content-type=', $response->headers->get('Content-Type'),
', isi=', $response->getContent(), PHP_EOL;
}Output yang Dihasilkan:
laporan_hilang_json: status=404, content-type=application/json, isi={
"message": "Laporan tidak ditemukan."
}
laporan_hilang_html: status=404, content-type=text/html; charset=utf-8Route api menghasilkan JSON berkat header yang dipasang middleware; route web tanpa middleware tetap menerima HTML.
Filter 3: Request Logger #
Filter ketiga mencatat jalannya setiap request ke log channel dengan Log::info:
// app/Http/Middleware/RequestLogger.php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
use Symfony\Component\HttpFoundation\Response;
class RequestLogger
{
public function handle(Request $request, Closure $next): Response
{
$mulai = microtime(true);
$response = $next($request);
Log::info('http-request', [
'path' => '/'.$request->path(),
'status' => $response->getStatusCode(),
'durasi_ms' => round((microtime(true) - $mulai) * 1000, 1),
]);
return $response;
}
}Waktu mulai dicatat sebelum pipeline lanjut, dan entri log ditulis setelah response kembali — satu entri berisi path, status, dan durasi. Pemanggilan nyata via route /api/laporan/9 meninggalkan baris berikut di storage/logs/laravel.log (waktu dan durasi_ms berubah di setiap eksekusi):
[2026-10-13 19:05:45] local.INFO: http-request {"path":"/api/laporan/9","status":200,"durasi_ms":1.7}Di feature test kita menangkap entri tersebut dengan mock Log::shouldReceive('info') agar tidak bergantung pada isi file log:
Log::shouldReceive('info')->once()->with(
'http-request',
\Mockery::on(function (array $context) {
printf("log http-request: path=%s status=%d\n", $context['path'], $context['status']);
return $context['path'] === '/api/laporan/9'
&& $context['status'] === 200
&& isset($context['durasi_ms']);
}),
);Output yang Dihasilkan:
log http-request: path=/api/laporan/9 status=200Menggabungkan Ketiganya #
Route /api/laporan/{id} memasang ketiga filter sekaligus, dan urutannya menentukan hasilnya:
// routes/api.php
Route::get('/laporan/{id}', function (string $id) {
if ($id !== '9') {
abort(404, 'Laporan tidak ditemukan.');
}
return ['id' => 9, 'judul' => 'Laporan Penjualan 2025'];
})->middleware([IpWhitelist::class, ForceJsonResponse::class, RequestLogger::class]);Saat IP terblokir, IpWhitelist melakukan short-circuit sebelum dua middleware di belakangnya sempat berjalan — sehingga logger pun tidak mencatat apa pun. Test berikut membuktikan efek urutan tersebut:
public function test_ip_terblokir_tidak_memicu_logger(): void
{
config(['whitelist.ips' => ['10.0.0.1']]);
Log::shouldReceive('info')->never();
$response = $this->get('/api/laporan/9');
$response->assertForbidden();
}Test tersebut lolos: response berstatus 403 dan ekspektasi never() terpenuhi — Log::info tidak pernah terpanggil karena RequestLogger tidak pernah diberi kesempatan berjalan. Bandingkan dengan kasus IP terdaftar pada output sebelumnya, di mana logger aktif mencatat.
Tiga filter ini mengakhiri section middleware: kita sudah bisa berpikir dalam lapisan, menulisnya, mendaftarkannya, dan merangkainya menjadi kebijakan aplikasi yang teruji. Kebutuhan berikutnya yang lazim menyertai middleware kelas berat seperti rate limiting dan session sudah dibahas pada section masing-masing. Rujukan resmi tersedia di dokumentasi middleware dan dokumentasi logging .