Rute API adalah kumpulan endpoint yang melayani data JSON, bukan halaman HTML, sehingga menjadi antarmuka bagi frontend terpisah maupun aplikasi pihak ketiga. Topik ini membahas berkas routes/api.php yang menampung rute tersebut, pendaftarannya pada bootstrap/app.php, perbedaan grup middleware web dan api, serta sifat stateless yang menjadi ciri permintaan API. Seluruh mekanisme diuji pada aplikasi catatan sehingga perilaku permintaan API dapat diamati dari keluaran nyata.
Berkas routes/api.php#
Proyek Laravel modern tidak menyertakan routes/api.php sejak awal, sehingga berkas ini baru muncul setelah perintah php artisan install:api dijalankan. Perintah tersebut melakukan tiga pekerjaan sekaligus, yaitu memasang paket autentikasi API bernama Sanctum, menerbitkan migration tabel token beserta berkas konfigurasinya, dan membuat routes/api.php yang langsung terdaftar pada aplikasi. Migration yang diterbitkan dijalankan setelahnya, sehingga tabel personal_access_tokens siap digunakan ketika autentikasi token dibahas.
# tidak-dijalankan: mengunduh paket dari jaringan dan memakan waktu
cd ~/aplikasi-catatan
php artisan install:api --without-migration-promptTanpa opsi --without-migration-prompt, perintah yang sama meminta konfirmasi untuk menjalankan seluruh migration tertunda. Perintah di atas dieksekusi pada proyek uji, dan dua baris penutup keluarannya menyatakan sebagai berikut.
INFO Published API routes file.
INFO API scaffolding installed. Please add the [Laravel\Sanctum\HasApiTokens] trait to your User model.Berkas yang dihasilkan masih berupa rute contoh dari framework, yaitu satu rute /user yang mengembalikan data pengguna yang sedang diautentikasi. Middleware auth:sanctum yang melekat pada rute itu menuntut token yang sah, sehingga tanpa token responsnya adalah 401. Isi berkasnya terlihat seperti berikut.
// tidak-dijalankan
<?php
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;
Route::get('/user', function (Request $request) {
return $request->user();
})->middleware('auth:sanctum');Pendaftaran berkas ini dilakukan pada bootstrap/app.php. Argumen api: pada metode withRouting() menandai berkas tersebut sebagai penampung rute API. Metode yang sama juga mendaftarkan routes/web.php dan routes/console.php, sehingga seluruh pendaftaran rute berada pada satu tempat.
// tidak-dijalankan
->withRouting(
web: __DIR__.'/../routes/web.php',
api: __DIR__.'/../routes/api.php',
commands: __DIR__.'/../routes/console.php',
health: '/up',
)Rute contoh kemudian ditimpa dengan rute milik aplikasi catatan. Rute-rute yang ditulis masih berupa fungsi tutup (closure) agar fokus tetap pada mekanisme rute, sedangkan rute /catatan/{id} dengan parameter mengembalikan satu catatan dalam bentuk JSON.
# menimpa isi routes/api.php dengan rute milik aplikasi catatan
cd ~/aplikasi-catatan
cat > routes/api.php <<'PHP'
<?php
use App\Models\Category;
use App\Models\Note;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;
Route::get('/catatan', function () {
return Note::where('category_id', Category::where('nama', 'Arsip')->value('id'))
->orderBy('judul')
->get(['judul']);
});
Route::get('/catatan/{id}', function (string $id) {
return Note::findOrFail($id);
});
Route::post('/catatan', function (Request $request) {
$data = $request->validate([
'judul' => ['required', 'string', 'max:100'],
'isi' => ['required', 'string'],
'kategori' => ['required', 'string'],
]);
$kategori = Category::firstOrCreate(['nama' => $data['kategori']]);
Note::firstOrCreate(
['judul' => $data['judul']],
['isi' => $data['isi'], 'category_id' => $kategori->id]
);
return response()->json([
'pesan' => 'Catatan tersimpan.',
'judul' => $data['judul'],
], 201);
});
PHPData contoh untuk kategori Arsip disiapkan secara deterministik agar keluaran pada topik ini selalu sama. Kategori dan tiga catatannya diberi id tetap, dan penanda waktunya ditetapkan pada nilai konstan, sehingga keluaran JSON yang memuat id, category_id, created_at, dan updated_at tidak berubah antar eksekusi.
# menyiapkan data contoh pada kategori Arsip dengan id yang dipatok
cd ~/aplikasi-catatan
php artisan tinker --execute='
use App\Models\Note;
use Illuminate\Support\Facades\DB;
DB::table("notes")->delete();
DB::statement("DELETE FROM sqlite_sequence WHERE name = \"notes\"");
DB::table("categories")->delete();
DB::table("categories")->insert(["id" => 4, "nama" => "Arsip", "created_at" => "2026-01-05 08:00:00", "updated_at" => "2026-01-05 08:00:00"]);
$data = [
[5, "Daftar tugas harian", "Membagi pekerjaan besar menjadi langkah yang selesai dalam satu hari."],
[6, "Pengingat rapat", "Menyiapkan poin pembahasan sebelum rapat dimulai."],
[7, "Rencana mingguan", "Menyusun prioritas pekerjaan untuk minggu berikutnya."],
];
foreach ($data as [$id, $judul, $isi]) {
DB::table("notes")->insert(["id" => $id, "judul" => $judul, "isi" => $isi, "category_id" => 4, "created_at" => "2026-01-05 08:00:00", "updated_at" => "2026-01-05 08:00:00"]);
}
echo Note::where("category_id", 4)->count()." catatan pada kategori Arsip".PHP_EOL;
'3 catatan pada kategori ArsipPrefix api dan pemeriksaan rute#
Ketika berkas api terdaftar melalui argumen api:, seluruh rute di dalamnya otomatis diberi awalan /api dan grup middleware api. URI pada berkas ditulis tanpa awalan tersebut, misalnya /catatan, dan endpoint-nya diakses sebagai /api/catatan. Perintah route:list memperlihatkan penerapan awalan tersebut.
# memeriksa rute api yang terdaftar
cd ~/aplikasi-catatan
php artisan route:list --path=api/catatan GET|HEAD api/catatan .................................... routes/api.php:8
POST api/catatan ................................... routes/api.php:18
GET|HEAD api/catatan/{id} .............................. routes/api.php:14
Showing [3] routesKetiga rute ditulis sebagai /catatan pada berkasnya, tetapi route:list menampilkannya sebagai api/catatan. Kolom aksi juga menunjukkan berkas dan baris asal setiap rute, sehingga daftar rute yang panjang tetap mudah dilacak. Perilaku lengkapnya diuji dengan skrip berikut yang mengirim permintaan HTTP tanpa server web, satu untuk membaca daftar, satu untuk membaca satu catatan, dan dua untuk menyimpan dengan data lengkap maupun data yang gagal validasi.
<?php
use App\Models\Note;
use Illuminate\Http\Request;
$root = getcwd();
if (! is_file($root.'/bootstrap/app.php')) {
$root = getenv('HOME').'/aplikasi-catatan';
}
require $root.'/vendor/autoload.php';
$app = require $root.'/bootstrap/app.php';
$app->make(Illuminate\Contracts\Console\Kernel::class)->bootstrap();
$kernel = $app->make(Illuminate\Contracts\Http\Kernel::class);
// skenario 1: daftar judul catatan pada kategori Arsip
$resp = $kernel->handle(Request::create('http://localhost/api/catatan', 'GET'));
echo '1. GET /api/catatan: status '.$resp->getStatusCode().PHP_EOL;
echo $resp->getContent().PHP_EOL;
// skenario 2: satu catatan berdasarkan id
$id = Note::where('judul', 'Rencana mingguan')->value('id');
$resp = $kernel->handle(Request::create('http://localhost/api/catatan/'.$id, 'GET'));
echo '2. GET /api/catatan/{id}: status '.$resp->getStatusCode().PHP_EOL;
echo $resp->getContent().PHP_EOL;
// skenario 3: menyimpan catatan baru dengan data lengkap
$resp = $kernel->handle(Request::create('http://localhost/api/catatan', 'POST', [], [], [], [
'CONTENT_TYPE' => 'application/json',
'HTTP_ACCEPT' => 'application/json',
], json_encode([
'judul' => 'Rangkuman materi',
'isi' => 'Poin penting dari materi yang dipelajari.',
'kategori' => 'Belajar',
])));
echo '3. POST /api/catatan: status '.$resp->getStatusCode().PHP_EOL;
echo $resp->getContent().PHP_EOL;
// skenario 4: menyimpan tanpa judul sehingga validasi gagal
$resp = $kernel->handle(Request::create('http://localhost/api/catatan', 'POST', [], [], [], [
'CONTENT_TYPE' => 'application/json',
'HTTP_ACCEPT' => 'application/json',
], json_encode([
'isi' => 'Poin penting dari materi yang dipelajari.',
'kategori' => 'Belajar',
])));
echo '4. POST tanpa judul: status '.$resp->getStatusCode().PHP_EOL;
echo $resp->getContent().PHP_EOL;1. GET /api/catatan: status 200
[{"judul":"Daftar tugas harian"},{"judul":"Pengingat rapat"},{"judul":"Rencana mingguan"}]
2. GET /api/catatan/{id}: status 200
{"id":7,"judul":"Rencana mingguan","isi":"Menyusun prioritas pekerjaan untuk minggu berikutnya.","category_id":4,"created_at":"2026-01-05T08:00:00.000000Z","updated_at":"2026-01-05T08:00:00.000000Z"}
3. POST /api/catatan: status 201
{"pesan":"Catatan tersimpan.","judul":"Rangkuman materi"}
4. POST tanpa judul: status 422
{"message":"The judul field is required.","errors":{"judul":["The judul field is required."]}}Skenario pertama menunjukkan daftar judul yang dikembalikan sebagai array JSON dengan status 200. Skenario kedua mengembalikan satu model Eloquent apa adanya, lengkap dengan seluruh kolomnya, termasuk kolom waktu. Skenario ketiga lolos validasi sehingga dijawab dengan 201 beserta isi yang ditetapkan pada rute, sedangkan skenario keempat dijawab dengan 422 beserta pesan error per kolom. Permintaan POST pada kedua skenario terakhir dikirim tanpa token CSRF dan tetap diloloskan. Perilaku itu dibahas pada bagian berikutnya.
Sifat stateless#
Permintaan API pada Laravel bersifat stateless, yaitu server tidak menyimpan keadaan apa pun antar permintaan. Tidak ada session yang dijalankan, tidak ada cookie yang dikirim, dan setiap permintaan harus membawa seluruh informasi yang dibutuhkan, termasuk bukti identitasnya sendiri. Sifat ini ditentukan oleh pilihan middleware karena grup api tidak memuat lapisan session dan proteksi CSRF yang dimiliki grup web, sehingga setiap permintaan diperlakukan sebagai permintaan yang berdiri sendiri.
Perbedaan isi kedua grup dapat diamati dengan membandingkan middleware yang diterapkan pada dua rute uji, satu dari grup web dan satu dari grup api.
# membandingkan middleware grup web dan api beserta akibatnya
cd ~/aplikasi-catatan
php artisan tinker --execute='
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;
Route::middleware("web")->name("uji.web")->post("/uji/state/web", fn () => response()->json(["pesan" => "lolos"]));
Route::middleware("api")->name("uji.api")->post("/uji/state/api", fn () => response()->json(["pesan" => "lolos"]));
$router = app("router");
$rute = Route::getRoutes();
echo "rute web: ".implode(", ", $router->gatherRouteMiddleware($rute->getByName("uji.web"))).PHP_EOL;
echo "rute api: ".implode(", ", $router->gatherRouteMiddleware($rute->getByName("uji.api"))).PHP_EOL;
$kernel = app()->make(Illuminate\Contracts\Http\Kernel::class);
$web = $kernel->handle(Request::create("http://localhost/uji/state/web", "POST"));
echo "status POST rute web: ".$web->getStatusCode().PHP_EOL;
$api = $kernel->handle(Request::create("http://localhost/uji/state/api", "POST"));
echo "status POST rute api: ".$api->getStatusCode().PHP_EOL;
echo "header Set-Cookie rute web: ".($web->headers->get("Set-Cookie") !== null ? "ada" : "tidak ada").PHP_EOL;
echo "header Set-Cookie rute api: ".($api->headers->get("Set-Cookie") !== null ? "ada" : "tidak ada").PHP_EOL;
'rute web: Illuminate\Cookie\Middleware\EncryptCookies, Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse, Illuminate\Session\Middleware\StartSession, Illuminate\View\Middleware\ShareErrorsFromSession, Illuminate\Foundation\Http\Middleware\PreventRequestForgery, Illuminate\Routing\Middleware\SubstituteBindings
rute api: Illuminate\Routing\Middleware\SubstituteBindings
status POST rute web: 419
status POST rute api: 200
header Set-Cookie rute web: ada
header Set-Cookie rute api: tidak adaKeluaran tersebut memperlihatkan tiga akibat langsung dari perbedaan grup. Pertama, rute web melewati enam middleware, sedangkan rute api hanya melewati satu, yaitu SubstituteBindings yang menggantikan parameter rute dengan nilai aslinya. Kedua, POST tanpa token CSRF ditolak grup web dengan status 419, tetapi diloloskan grup api dengan status 200. Ketiga, respons rute web mengirim header Set-Cookie berisi cookie session, sedangkan respons rute api tidak mengirim cookie sama sekali. Alur kedua permintaan tersebut digambarkan sebagai berikut.
Kesalahan umum#
Kesalahan pertama adalah memanggil data session pada rute API. Fungsi session() atau helper old() yang dipanggil di dalam rute api melempar error Session store not found on the request handler karena grup api tidak memuat StartSession. Perbaikannya adalah memindahkan data sementara tersebut ke database, atau menggunakan pesan flash hanya pada rute yang berada di grup web.
Kesalahan kedua adalah menambahkan awalan /api secara manual pada rute di routes/api.php. Awalan /api sudah diberikan secara otomatis oleh pendaftaran berkas pada bootstrap/app.php, sehingga penulisan Route::prefix('api') di dalam berkas itu menghasilkan endpoint /api/api/catatan. Perbaikannya adalah menulis URI tanpa awalan apa pun dan membiarkan framework menambahkannya.
Kesalahan ketiga adalah mengembalikan pengalihan (redirect) dari rute API. Klien yang memanggil endpoint, misalnya aplikasi frontend, tidak mengikuti pengalihan seperti browser, sehingga status 302 diterima apa adanya dan data yang diminta tidak pernah sampai. Perbaikannya adalah mengembalikan respons JSON yang menyatakan keadaan secara eksplisit, misalnya 201 beserta data yang baru dibuat.
Ringkasan#
- Berkas
routes/api.phpdibuat oleh perintahphp artisan install:apidan didaftarkan padabootstrap/app.phpmelalui argumenapi:yang sekaligus memberi awalan/api. - Grup middleware
apitidak memuat session dan proteksi CSRF, sehingga permintaan API bersifat stateless, tanpa cookie, dan setiap POST diperlakukan sebagai permintaan baru. - Keluaran rute API adalah JSON dengan status yang bermakna, yaitu 200 untuk pembacaan, 201 untuk pembuatan, dan 422 untuk kegagalan validasi.
- Data sementara tidak dapat disimpan di session pada rute API, sehingga keadaan harus disimpan di database atau dibawa oleh klien pada setiap permintaan.
Referensi#
- Routing — Laravel — Dokumentasi pendaftaran berkas rute api, prefix, dan parameter rute.
- Middleware — Laravel — Dokumentasi grup middleware dan pemeriksaannya melalui route:list.