Mengembalikan model mentah dari controller cepat, tetapi boros dan berbahaya: semua kolom tampil apa adanya, nama kolom database bocor ke kontrak publik, dan relasi ikut-ikutan tergantung kebiasaan pemanggil. Laravel menjawab masalah ini dengan API resource class — lapisan transformasi tersendiri antara model dan JSON. Chapter ini membangun PostResource untuk mengendalikan bentuk output, menambahkan field kondisional lewat when() dan whenLoaded(), merangkai relasi bersarang, sampai menyerahkan meta pagination kepada client.

Membuat Resource dengan make:resource #

Dua perintah artisan menyiapkan semua kelas yang kita butuhkan — satu untuk satu post, satu untuk koleksi:

bash
php artisan make:resource PostResource
php artisan make:resource CommentResource
php artisan make:resource PostCollection

Output yang Dihasilkan:

text
   INFO  Resource [app/Http/Resources/PostResource.php] created.

   INFO  Resource [app/Http/Resources/CommentResource.php] created.

   INFO  Resource collection [app/Http/Resources/PostCollection.php] created.

Kerangka PostResource yang dihasilkan adalah JsonResource dengan satu method wajib toArray():

php
<?php

namespace App\Http\Resources;

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

class PostResource extends JsonResource
{
    /**
     * Transform the resource into an array.
     *
     * @return array<string, mixed>
     */
    public function toArray(Request $request): array
    {
        return parent::toArray($request);
    }
}

PostCollection lahir sebagai ResourceCollection — kerangka yang sama, tugas sedikit berbeda: membungkus banyak resource sekaligus.

Struktur toArray #

Method toArray() adalah tempat kita mendikte bentuk JSON. Untuk contoh ini, model Post memiliki relasi author() (milik User) dan comments() (banyak Comment). Kita tulis resource-nya:

php
<?php

// app/Http/Resources/CommentResource.php

namespace App\Http\Resources;

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

class CommentResource extends JsonResource
{
    public function toArray(Request $request): array
    {
        return [
            'id' => $this->id,
            'author' => $this->author,
            'body' => $this->body,
        ];
    }
}
php
<?php

// app/Http/Resources/PostResource.php

namespace App\Http\Resources;

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

class PostResource extends JsonResource
{
    public function toArray(Request $request): array
    {
        return [
            'id' => $this->id,
            'judul' => $this->title,
            'konten' => $this->content,
            'ringkasan' => $this->when(
                $request->boolean('ringkas'),
                fn () => Str::limit($this->content, 20),
            ),
            'penulis' => $this->whenLoaded('author', fn () => [
                'nama' => $this->author->name,
                'email' => $this->author->email,
            ]),
            'komentar' => CommentResource::collection($this->whenLoaded('comments')),
        ];
    }
}

Perhatikan tiga teknik yang bekerja di dalamnya. Pertama, pemetaan kolom ke kunci publik (title → judul) — nama internal database berhenti di resource ini. Kedua, when() menyertakan field hanya bila kondisinya benar. Ketiga, whenLoaded() menyertakan relasi hanya bila query sudah mengeager-load-nya — mencegah N+1 query tersembunyi saat serialisasi koleksi besar.

Field Kondisional dalam Praktik #

Kita daftarkan route pengguna resource, lalu bandingkan pemanggilan dengan dan tanpa relasi ter-load:

php
<?php

// routes/api.php (di dalam group prefix v1)

use App\Http\Resources\PostCollection;
use App\Http\Resources\PostResource;
use App\Models\Post;
use Illuminate\Support\Facades\Route;

Route::get('/posts-resource', fn () => new PostCollection(Post::latest('id')->get()));

Route::get('/posts-resource/{post}', fn (Post $post) => new PostResource($post));

Route::get('/posts-lengkap/{post}', fn (string $post) => new PostResource(
    Post::with(['author', 'comments'])->findOrFail($post)
));

Output yang Dihasilkan:

text
### GET /api/v1/posts-resource/1 → HTTP 200
{
    "data": {
        "id": 1,
        "judul": "Belajar API Resource",
        "konten": "JsonResource membentuk output JSON."
    }
}

### GET /api/v1/posts-resource/1?ringkas=true → HTTP 200
{
    "data": {
        "id": 1,
        "judul": "Belajar API Resource",
        "konten": "JsonResource membentuk output JSON.",
        "ringkasan": "JsonResource membent..."
    }
}

### GET /api/v1/posts-lengkap/1 (author + comments di-eager load) → HTTP 200
{
    "data": {
        "id": 1,
        "judul": "Belajar API Resource",
        "konten": "JsonResource membentuk output JSON.",
        "penulis": {
            "nama": "Budi",
            "email": "[email protected]"
        },
        "komentar": [
            {
                "id": 1,
                "author": "Siti",
                "body": "Membantu sekali!"
            }
        ]
    }
}

Tiga pemanggilan ini memperlihatkan field kondisional bekerja: tanpa ?ringkas=true kunci ringkasan lenyap dari JSON (bukan null, melainkan tidak ada), dan tanpa eager loading kunci penulis serta komentar ikut hilang. Nested resource juga terlihat — CommentResource::collection() merangkai relasi satu-ke-banyak dengan bentuk yang sama konsisten.

Collection dengan Meta Kustom #

PostCollection membungkus banyak post dengan collects yang menunjuk resource pemeta, plus meta tambahan milik kita:

php
<?php

// app/Http/Resources/PostCollection.php

namespace App\Http\Resources;

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

class PostCollection extends ResourceCollection
{
    public $collects = PostResource::class;

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

Output yang Dihasilkan:

text
### GET /api/v1/posts-resource → HTTP 200
{
    "data": [
        {
            "id": 1,
            "judul": "Belajar API Resource",
            "konten": "JsonResource membentuk output JSON."
        }
    ],
    "meta": {
        "total_post": 1
    }
}

Meta Pagination Otomatis #

Kekuatan sesungguhnya muncul ketika collection class menerima paginator, bukan koleksi biasa. Route berikut memakai paginate(), dan resource otomatis menambahkan blok links serta meta pagination:

php
<?php

// routes/api.php (di dalam group prefix v1)

Route::get('/posts-halaman', fn () => PostResource::collection(Post::latest('id')->paginate(5)));

Dengan 12 post di database, halaman kedua berbentuk:

Output yang Dihasilkan:

text
### GET /api/v1/posts-halaman?page=2 → HTTP 200
{
    "data": [
        {
            "id": 7,
            "judul": "Post ke-7",
            "konten": "Konten post."
        },
        {
            "id": 6,
            "judul": "Post ke-6",
            "konten": "Konten post."
        },
        {
            "id": 5,
            "judul": "Post ke-5",
            "konten": "Konten post."
        },
        {
            "id": 4,
            "judul": "Post ke-4",
            "konten": "Konten post."
        },
        {
            "id": 3,
            "judul": "Post ke-3",
            "konten": "Konten post."
        }
    ],
    "links": {
        "first": "http://localhost/api/v1/posts-halaman?page=1",
        "last": "http://localhost/api/v1/posts-halaman?page=3",
        "prev": "http://localhost/api/v1/posts-halaman?page=1",
        "next": "http://localhost/api/v1/posts-halaman?page=3"
    },
    "meta": {
        "current_page": 2,
        "from": 6,
        "last_page": 3,
        "path": "http://localhost/api/v1/posts-halaman",
        "per_page": 5,
        "to": 10,
        "total": 12
    }
}

Client kini bisa membangun navigasi halaman tanpa memhitung sendiri: next untuk maju, last_page untuk tahu ujung, total untuk menampilkan jumlah keseluruhan. Output di atas sengaja dipangkas: respons aslinya masih memuat array meta.links berisi tautan navigasi per nomor halaman (« Previous, 1, 2, 3, Next ») — sama seperti tautan pagination di halaman web, hanya dalam bentuk JSON.

Transformasi JSON kini berada di tangan resource class yang rapi dan teruji. Tetapi ujung masuk API — request yang dikirim client — belum kita santuni dengan kekhususan yang sama: validasi payload JSON beserta format error 422-nya adalah topik chapter API Validation . Rujukan lengkap semua fitur resource tersedia di dokumentasi eloquent-resources .