API Resource: Transformasi Response JSON
Pisahkan bentuk response API dari struktur database.
Masalah: Model Bukan Format Response
Godaan paling besar saat membangun API adalah return Post::all();. Satu baris, langsung jadi JSON. Tapi kamu baru saja melakukan tiga kesalahan sekaligus: membocorkan struktur database (nama kolom, relasi internal) ke publik, mengekspos field sensitif kalau modelnya User (password hash aman karena $hidden, tapi remember_token dan kolom internal lain ikut terkirim), dan mengunci kontrak API ke skema tabel. Begitu kamu rename kolom database, frontend ikut rusak. API Resource memisahkan bentuk response dari model database.
Membuat Resource
php artisan make:resource PostResource// app/Http/Resources/PostResource.php
namespace App\Http\Resources;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
class PostResource extends JsonResource
{
public function toArray(Request $request): array
{
return [
'id' => $this->id,
'judul' => $this->title,
'slug' => $this->slug,
'ringkasan' => str($this->body)->limit(150),
'penulis' => $this->user->name,
'diterbitkan_pada' => $this->published_at?->toDateString(),
'jumlah_komentar' => $this->whenCounted('comments'),
'links' => [
'self' => route('api.posts.show', $this->id),
],
];
}
}Perhatikan apa yang terjadi di sini: nama field boleh beda dari kolom database (judul dari title), value boleh hasil komputasi (ringkasan), dan field sensitif tidak pernah disebut sehingga tidak pernah terkirim. Frontend hanya tahu kontrak ini, bukan skema tabelmu.
Pakai di controller:
// app/Http/Controllers/Api/PostController.php
public function index()
{
return PostResource::collection(
Post::with('user')->withCount('comments')->latest()->paginate(10)
);
}
public function show(Post $post)
{
return new PostResource($post->load(['user', 'comments.user']));
}PostResource::collection() otomatis membungkus setiap item plus mempertahankan struktur pagination (data, links, meta).
Conditional Attribute: Senjata Anti N+1
Jebakan terbesar di Resource: mengakses $this->user->name langsung. Kalau controller lupa eager load user, setiap item collection memicu satu query lazy load. Inilah N+1 yang bersembunyi di lapisan transformasi. Solusinya adalah attribute kondisional:
public function toArray(Request $request): array
{
return [
'id' => $this->id,
'judul' => $this->title,
// hanya muncul kalau relasi 'user' sudah di-load, tanpa query tambahan
'penulis' => $this->whenLoaded('user', fn () => $this->user->name),
// hanya untuk admin yang request
'email_penulis' => $this->when(
$request->user()?->is_admin,
fn () => $this->user->email
),
// gabungkan beberapa field kondisional sekaligus
$this->mergeWhen($request->user()?->is_admin, [
'dilihat' => $this->views_count,
'ip_terakhir' => $this->last_ip,
]),
];
}whenLoaded() mengembalikan MissingValue yang otomatis dihapus dari JSON kalau relasi belum di-load. Tidak ada query diam-diam, tidak ada error.
Resource Collection Khusus
Kalau transformasi koleksi butuh meta tambahan (misal total keseluruhan), buat resource collection:
php artisan make:resource PostCollection// app/Http/Resources/PostCollection.php
public function toArray(Request $request): array
{
return [
'data' => $this->collection,
'meta' => [
'total_published' => Post::whereNotNull('published_at')->count(),
],
];
}Secara default Laravel membungkus resource tunggal dalam key data. Kalau kamu mau response flat tanpa wrapper:
// di AppServiceProvider atau controller
PostResource::withoutWrapping();Hati-hati: mematikan wrapping mengubah kontrak API, lakukan hanya kalau frontend-mu memang mengharapkan format flat sejak awal.
Jebakan Umum
Pertama, komputasi berat di toArray(). Method ini dipanggil per item, jadi $this->comments()->count() di dalam resource untuk 50 item berarti 50 query. Pindahkan agregat ke withCount() di controller dan baca via whenCounted().
Kedua, memakai satu Resource untuk semua konteks. Response index (ringkas) dan show (detail lengkap) idealnya beda: PostResource untuk daftar, PostDetailResource untuk single dengan relasi penuh. Memaksa satu resource untuk keduanya berujung pada field yang setengah relevan di mana-mana.
Ketiga, mengekspos pivot atau field internal relasi many-to-many tanpa sadar. Kalau resource mengembalikan relasi mentah, data pivot ikut terkirim. Selalu transform relasi lewat resource-nya sendiri (TagResource::collection($this->tags)).
Catatan teknis: Resource adalah kontrak API-mu. Perlakukan perubahannya seperti perubahan kontrak: menambah field itu aman (backward compatible), menghapus atau rename field itu breaking change. Kalau kamu butuh mengubah bentuk response secara drastis, buat
PostResourceV2daripada mengedit yang lama dan merusak client yang sudah ada.
Tantangan
Resource User
Buat UserResource yang menampilkan id, name, email TAPI menyembunyikan password dan remember_token, plus menghitung posts_count. Test via route API dan cek JSON-nya.