Membaca satu response adalah keterampilan; mengonsumsi sebuah REST API adalah kebiasaan. Endpoint nyata tidak pernah mengirim semua data sekaligus — hasil datang terpotong per halaman, lengkap dengan filter, meta, dan batas laju. Chapter ini merangkai semuanya menjadi satu konsumen utuh: kita membangun klien untuk “task API” milik tim lain, menjelajahi semua halamannya, memetakan JSON ke model Eloquent Task, menghormati rate limit, dan menutup dengan pola pagination berbasis cursor. Seluruh contoh memakai fake yang meniru perilaku API sungguhan sehingga output-nya deterministik.
Menyiapkan Tabel Penampung #
Data dari API eksternal layak punya rumah sendiri di database kita. Migration berikut membuat tabel tasks dengan external_id unik — identifier dari API sumber, terpisah dari primary key lokal:
<?php
// database/migrations/2026_10_13_000000_create_tasks_table.php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('tasks', function (Blueprint $table) {
$table->id();
$table->unsignedBigInteger('external_id')->unique();
$table->string('judul');
$table->string('status')->default('todo');
$table->timestamps();
});
}
public function down(): void
{
Schema::dropIfExists('tasks');
}
};Model-nya sederhana, dengan tiga kolom yang boleh diisi massal:
<?php
// app/Models/Task.php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Task extends Model
{
protected $fillable = ['external_id', 'judul', 'status'];
}Klien Sinkronisasi dengan Pagination #
API contoh kita mengembalikan maksimal dua task per halaman dengan meta last_page, dan menerima parameter query page, per_page, serta status untuk filter. Berikut klien yang menjelajahi seluruh halaman dan memetakan setiap baris lewat updateOrCreate:
<?php
use App\Models\Task;
use Illuminate\Support\Facades\Http;
function sinkronisasiTugas(?string $status = null): int
{
$halaman = 1;
$total = 0;
do {
$response = Http::get('https://api.example.com/tasks', array_filter([
'page' => $halaman,
'per_page' => 2,
'status' => $status,
]));
foreach ($response->json('data') as $baris) {
Task::updateOrCreate(
['external_id' => $baris['id']],
['judul' => $baris['title'], 'status' => $baris['status']],
);
$total++;
}
$halaman++;
} while ($halaman <= $response->json('meta.last_page'));
return $total;
}Loop do...while bergerak dari halaman satu sampai meta.last_page; array_filter membuang key status saat bernilai null agar parameter filter hanya terkirim bila diminta. Di dalam loop, updateOrCreate memegang peran ganda: baris baru di-insert, baris lama di-update — keduanya lewat satu panggilan. Pemetaan id → external_id dan title → judul terjadi tepat di argumen method ini, menjadikannya satu-satunya tempat yang perlu dibaca saat bentuk payload vendor berubah.
Untuk pengujian, fake berikut meniru API lima task lengkap dengan logika slice per halaman — parse_str memecah query string dari URL request, lalu collection when() menerapkan filter status bila ada:
<?php
// Fake yang meniru "task API": slice data sesuai page + per_page + status
$semua = [
['id' => 101, 'title' => 'Belajar HTTP Client', 'status' => 'selesai'],
['id' => 102, 'title' => 'Belajar Response', 'status' => 'selesai'],
['id' => 103, 'title' => 'Belajar Retry', 'status' => 'todo'],
['id' => 104, 'title' => 'Belajar Mocking', 'status' => 'todo'],
['id' => 105, 'title' => 'Belajar Pool', 'status' => 'todo'],
];
Http::fake(function ($request) use ($semua) {
parse_str(parse_url($request->url(), PHP_URL_QUERY), $query);
$hasil = collect($semua)
->when($query['status'] ?? null, fn ($c) => $c->where('status', $query['status']))
->values();
return Http::response([
'data' => $hasil->slice(($query['page'] - 1) * $query['per_page'], $query['per_page'])->values()->all(),
'meta' => ['last_page' => (int) ceil($hasil->count() / $query['per_page'])],
], 200);
});
echo 'task tersimpan: '.sinkronisasiTugas()."\n";
foreach (Task::orderBy('external_id')->get() as $t) {
echo $t->external_id.' | '.$t->judul.' | '.$t->status."\n";
}Output yang Dihasilkan:
task tersimpan: 5
101 | Belajar HTTP Client | selesai
102 | Belajar Response | selesai
103 | Belajar Retry | todo
104 | Belajar Mocking | todo
105 | Belajar Pool | todoDua hal terbukti dari output: halaman terpotong benar (lima task tiba lewat tiga halaman), dan kolom JSON title terpetakan ke kolom lokal judul. Filter bekerja lewat jalur yang sama — cukup kirim argumen status:
<?php
echo 'task todo tersimpan: '.sinkronisasiTugas('todo')."\n";Output yang Dihasilkan:
task todo tersimpan: 3Hanya tiga task berstatus todo yang dikirim API, dan parameter status=todo hanya muncul di request itu — array_filter menjaga pemanggilan lain tetap polos. Sintesis idempotensi juga terlihat saat sinkronisasi diulang:
<?php
echo 'task tersimpan: '.sinkronisasiTugas()."\n";
echo 'total task di database (tidak duplikat): '.Task::count()."\n";Output yang Dihasilkan:
task tersimpan: 5
total task di database (tidak duplikat): 5updateOrCreate mencari baris berdasarkan external_id lalu memperbarui judulnya — task yang sama tidak pernah terduplikasi seberapa sering sinkronisasi berjalan.
Menghormati Rate Limit (429) #
API publik menjaga diri dengan batas laju: lewat batas, dan response berubah menjadi 429 dengan header Retry-After yang memberi tahu berapa detik kita harus menunggu. Pola penanganannya eksplisit — cek status, baca header, tunggu, coba lagi:
<?php
use Illuminate\Http\Client\Response;
use Illuminate\Support\Facades\Http;
function ambilDenganBatas(string $url, array $query = []): Response
{
$maksTunggu = 3;
$tunggu = 0;
while (true) {
$response = Http::get($url, $query);
if ($response->status() !== 429) {
return $response;
}
if ($tunggu >= $maksTunggu) {
return $response; // menyerah: kembalikan 429 ke pemanggil
}
$detik = (int) $response->header('Retry-After', 1);
sleep(min($detik, 2));
$tunggu++;
}
}Fake berikut mensimulasikan API yang menolak permintaan pertama lalu menerima yang kedua:
<?php
$permintaan = 0;
Http::fake(function () use (&$permintaan) {
$permintaan++;
if ($permintaan === 1) {
return Http::response(['pesan' => 'terlalu banyak permintaan'], 429, [
'Retry-After' => '1',
]);
}
return Http::response(['data' => [['id' => 101, 'title' => 'Belajar HTTP Client']]], 200);
});
$mula = microtime(true);
$r = ambilDenganBatas('https://api.example.com/tasks');
printf("status: %d (setelah %.1f detik)\n", $r->status(), microtime(true) - $mula);
echo 'task pertama: '.$r->json('data.0.title')."\n";
echo 'total permintaan: '.$permintaan."\n";Output yang Dihasilkan:
status: 200 (setelah 1.0 detik)
task pertama: Belajar HTTP Client
total permintaan: 2Permintaan pertama ditolak 429, klien menunggu satu detik sesuai Retry-After, dan permintaan kedua lolos.
Pagination Berbasis Cursor #
Pagination nomor halaman punya kelemahan: data yang berpindah selagi kita membalik halaman bisa terlewat atau terbaca dua kali. API modern menawarkan cursor — setiap response membawa next_cursor untuk memintanya:
<?php
use Illuminate\Support\Facades\Http;
function ambilSemuaCursor(): array
{
$hasil = [];
$cursor = null;
do {
$response = Http::get('https://api.example.com/tasks', array_filter([
'limit' => 2,
'cursor' => $cursor,
]));
$hasil = array_merge($hasil, $response->json('data'));
$cursor = $response->json('meta.next_cursor');
} while ($cursor !== null);
return $hasil;
}
foreach (ambilSemuaCursor() as $task) {
echo $task['id'].' | '.$task['title']."\n";
}
echo 'total: '.count(ambilSemuaCursor())."\n";Output yang Dihasilkan:
101 | Belajar HTTP Client
102 | Belajar Response
103 | Belajar Retry
105 | Belajar Pool
total: 4Loop berhenti saat next_cursor bernilai null — sinyal halaman habis. Dibanding nomor halaman, cursor menjaga posisi baca tetap konsisten meski data baru menyusul di tengah penjelajahan.
Klien kita kini berfungsi penuh sebagai konsumen API. Namanya masih fungsi lepas- lepasan; saat layanan pihak ketiga mulai bertambah, mereka layak dibungkus service class yang terstruktur — itu topik chapter Mengintegrasikan Layanan Eksternal .