API resource

Transformasi model Eloquent menjadi keluaran JSON yang terkendali.

Daftar isi

API resource adalah kelas khusus yang mengubah model Eloquent menjadi keluaran JSON dengan bentuk yang ditetapkan aplikasi, bukan sekadar salinan kolom tabel. Topik ini membahas pembuatan kelas resource tunggal beserta kelas collection-nya, pemasangannya pada rute API aplikasi catatan, dan atribut kondisional yang mengikuti relasi yang termuat. Seluruh mekanisme diuji dengan permintaan HTTP nyata sehingga keluaran model mentah dan keluaran resource dapat dibandingkan secara langsung.

Keterbatasan pengembalian model langsung#

Model Eloquent yang dikembalikan langsung dari rute API diserialisasi menurut daftar kolom tabelnya. JSON yang dihasilkan menyertakan created_at dan updated_at, memuat kolom penghubung seperti category_id, dan mengirim isi catatan utuh bahkan ketika daftar hanya membutuhkan judul. Bentuk semacam ini bergantung pada struktur tabel. Cukup menambahkan satu kolom internal pada tabel, dan bentuk respons ikut berubah tanpa satu baris kode rute pun yang disentuh.

API resource membalik arah tersebut. Bentuk JSON ditetapkan di satu kelas, digunakan ulang oleh seluruh rute yang menyajikan model yang sama, dan perubahan kontrak cukup dilakukan satu kali. Kelas resource juga menjadi tempat yang tepat untuk menggabungkan nilai dari beberapa kolom, menyembunyikan atribut tertentu, dan menyertakan data relasi saat model dirender.

Membuat kelas resource#

Kelas resource dapat dibuat dengan perintah php artisan make:resource NoteResource, yang menghasilkan berkas berisi kerangka metode toArray. Pada proyek uji, isi berkasnya ditulis lengkap sekaligus agar setiap eksekusi ulang menghasilkan berkas yang persis sama.

bash
# menuliskan kelas NoteResource pada proyek uji
cd ~/aplikasi-catatan
mkdir -p app/Http/Resources
cat > app/Http/Resources/NoteResource.php <<'PHP'
<?php

namespace App\Http\Resources;

use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
use Illuminate\Support\Str;

class NoteResource extends JsonResource
{
    public function toArray(Request $request): array
    {
        return [
            'id' => $this->id,
            'judul' => $this->judul,
            'ringkasan' => Str::limit($this->isi, 50),
            'kategori' => $this->whenLoaded('category', fn () => [
                'id' => $this->category->id,
                'nama' => $this->category->nama,
            ]),
        ];
    }
}
PHP

Metode toArray menerima permintaan yang sedang berjalan dan mengembalikan array asosiatif. Setiap kunci array menjadi kunci JSON, sedangkan nilai berasal dari kolom model maupun perhitungan tersendiri, misalnya Str::limit yang memotong isi catatan menjadi ringkasan lima puluh karakter. Metode whenLoaded menyertakan kunci hanya apabila relasi bernama category sudah termuat pada model, sehingga rute yang memuat relasi dan yang tidak memuatnya dapat berbagi kelas resource yang sama.

Resource tunggal memadai untuk satu model, sedangkan daftar data membutuhkan kelas turunan ResourceCollection. Kelas NoteCollection mengisi properti $collects dengan NoteResource agar setiap butir daftar diubah oleh resource yang sama, lalu menambahkan kunci jumlah yang berisi hitungan butir.

bash
# menuliskan kelas NoteCollection pada proyek uji
cd ~/aplikasi-catatan
mkdir -p app/Http/Resources
cat > app/Http/Resources/NoteCollection.php <<'PHP'
<?php

namespace App\Http\Resources;

use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\ResourceCollection;

class NoteCollection extends ResourceCollection
{
    public $collects = NoteResource::class;

    public function toArray(Request $request): array
    {
        return [
            'data' => $this->collection,
            'jumlah' => $this->collection->count(),
        ];
    }
}
PHP

Kunci data pada toArray ditulis secara eksplisit, dan Laravel tidak menambahkan pembungkus kedua ketika kunci itu sudah ada, sehingga hasil akhirnya tetap terbungkus satu lapis. Properti $this->collection berupa kumpulan model yang sudah diubah menjadi NoteResource satu per satu, sehingga setiap butir di dalamnya melewati transformasi yang sama. Kunci tambahan seperti jumlah diletakkan sejajar dengan data tanpa mengganggu isi daftarnya, sebagaimana terlihat pada keluaran topik ini.

Transformasi pada rute#

Rute mengembalikan resource dengan cara yang sama seperti mengembalikan model, yaitu objek tersebut ditulis sebagai nilai kembalian closure. Perbedaannya terletak pada penyiapan model, karena relasi yang hendak muncul dalam JSON dimuat terlebih dahulu dengan with('category') agar atribut kategorinya benar-benar tersedia saat resource dirender.

bash
# menimpa routes/api.php agar rute baca memakai resource
cd ~/aplikasi-catatan
cat > routes/api.php <<'PHP'
<?php

use App\Http\Resources\NoteCollection;
use App\Http\Resources\NoteResource;
use App\Models\Category;
use App\Models\Note;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;

Route::get('/catatan', function () {
    return new NoteCollection(
        Note::with('category')
            ->where('category_id', Category::where('nama', 'Arsip')->value('id'))
            ->orderBy('judul')
            ->get()
    );
});

Route::get('/catatan/{id}', function (string $id) {
    return new NoteResource(
        Note::with('category')->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);
});
PHP

Rute POST dibiarkan menggunakan response()->json karena responsnya hanya berupa pesan bahwa catatan tersimpan, bukan representasi model, sehingga tidak ada transformasi yang dibutuhkan.

Menguji transformasi#

Empat skenario berikut membandingkan keluaran mentah dengan keluaran resource pada proyek uji. Data contoh disiapkan secara deterministik di awal skrip, dengan id kategori, id catatan, dan penanda waktu yang ditetapkan tetap agar isi JSON tidak berubah antar eksekusi.

php uji-api-resource.php
<?php

use App\Http\Resources\NoteResource;
use App\Models\Category;
use App\Models\Note;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;

$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();

// data contoh disiapkan deterministik, id kategori dan catatan dipatok
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',
    ]);
}

$kernel = $app->make(Illuminate\Contracts\Http\Kernel::class);

// skenario 1: model dikembalikan apa adanya, tanpa resource
$note = Note::where('judul', 'Pengingat rapat')->first();
echo '1. model tanpa resource: '.json_encode($note).PHP_EOL;

// skenario 2: daftar catatan melalui NoteCollection
$resp = $kernel->handle(Request::create('http://localhost/api/catatan', 'GET'));
echo '2. GET /api/catatan: status '.$resp->getStatusCode().PHP_EOL;
echo $resp->getContent().PHP_EOL;

// skenario 3: satu catatan melalui NoteResource
$id = Note::where('judul', 'Rencana mingguan')->value('id');
$resp = $kernel->handle(Request::create('http://localhost/api/catatan/'.$id, 'GET'));
echo '3. GET /api/catatan/{id}: status '.$resp->getStatusCode().PHP_EOL;
echo $resp->getContent().PHP_EOL;

// skenario 4: resource tanpa eager loading, atribut relasi dihilangkan
$note = Note::findOrFail($id);
echo '4. resource tanpa eager loading: '.json_encode(new NoteResource($note)).PHP_EOL;
text Keluaran
1. model tanpa resource: {"id":6,"judul":"Pengingat rapat","isi":"Menyiapkan poin pembahasan sebelum rapat dimulai.","category_id":4,"created_at":"2026-01-05T08:00:00.000000Z","updated_at":"2026-01-05T08:00:00.000000Z"}
2. GET /api/catatan: status 200
{"data":[{"id":5,"judul":"Daftar tugas harian","ringkasan":"Membagi pekerjaan besar menjadi langkah yang seles...","kategori":{"id":4,"nama":"Arsip"}},{"id":6,"judul":"Pengingat rapat","ringkasan":"Menyiapkan poin pembahasan sebelum rapat dimulai.","kategori":{"id":4,"nama":"Arsip"}},{"id":7,"judul":"Rencana mingguan","ringkasan":"Menyusun prioritas pekerjaan untuk minggu berikutn...","kategori":{"id":4,"nama":"Arsip"}}],"jumlah":3}
3. GET /api/catatan/{id}: status 200
{"data":{"id":7,"judul":"Rencana mingguan","ringkasan":"Menyusun prioritas pekerjaan untuk minggu berikutn...","kategori":{"id":4,"nama":"Arsip"}}}
4. resource tanpa eager loading: {"id":7,"judul":"Rencana mingguan","ringkasan":"Menyusun prioritas pekerjaan untuk minggu berikutn..."}

Skenario pertama memperlihatkan model apa adanya dengan enam kolom, termasuk penanda waktu dan category_id. Skenario kedua menghasilkan daftar terbungkus data yang hanya memuat empat kunci per butir, disusul jumlah dari kelas NoteCollection. Skenario ketiga menegaskan bahwa resource tunggal juga terbungkus data tepat satu kali. Skenario keempat memanggil NoteResource pada model tanpa eager loading, dan kunci kategori menghilang tanpa pesan error apa pun karena whenLoaded hanya mengenali relasi yang termuat. Alur transformasinya digambarkan sebagai berikut.

Model Noteid, judul, isi,category_id,created_at,updated_atseluruh kolom tabelNoteResourcetoArray($request):pilih kolom,hitung ringkasan,whenLoaded('category')bentuk ditetapkan di siniJSON respons{"data": {"id","judul","ringkasan","kategori"}}kontrak untuk frontendBentuk JSON ditentukan oleh toArray, bukan oleh daftar kolom tabel.
Alur transformasi model menjadi JSON melalui API resource

Kesalahan umum#

Kesalahan pertama adalah menggunakan whenLoaded tanpa memuat relasinya. Atribut relasi menghilang dari JSON tanpa pesan error, sehingga kekurangan ini sering baru terlihat pada sisi frontend. Perbaikannya adalah memuat relasi dengan with() pada query, sebagaimana dilakukan oleh kedua rute GET pada topik ini.

Kesalahan kedua adalah membungkus data dua kali. Mengembalikan resource di dalam response()->json(['data' => ...]) menghasilkan pembungkus ganda data.data karena resource sudah dibungkus secara otomatis. Perbaikannya adalah mengembalikan resource apa adanya dari rute dan membiarkan Laravel yang membungkusnya.

Kesalahan ketiga adalah menempatkan logika berat di dalam toArray. Transformasi yang menjalankan query tambahan untuk setiap butir memunculkan kembali masalah N+1 meskipun bentuk JSON-nya rapi. Perbaikannya adalah menyiapkan seluruh data yang dibutuhkan pada query rute, misalnya melalui eager loading, dan menjadikan toArray murni pemetaan nilai.

Ringkasan#

  • API resource menetapkan bentuk JSON pada satu kelas melalui toArray, sehingga keluaran tidak lagi mengikuti daftar kolom tabel.
  • Nilai keluaran dapat berasal dari kolom, perhitungan seperti Str::limit, maupun relasi yang disertakan melalui whenLoaded.
  • Kelas turunan ResourceCollection menyediakan bentuk untuk daftar beserta kunci tambahan seperti jumlah.
  • Laravel membungkus keluaran resource tunggal maupun collection dalam kunci data tepat satu kali, dan whenLoaded menghilangkan kunci relasi apabila relasinya tidak termuat.

Referensi#