Membangun REST API di Laravel: route api.php, API Resources sebagai transformer, JSON responses yang konsisten, versioning, pagination, dan dokumentasi API dengan Scribe.

Saat ini hampir semua aplikasi punya sisi API — untuk mobile app, frontend SPA, integrasi pihak ketiga, atau sekadar memisahkan logika dari tampilan. Di episode ini kita bangun REST API di Laravel dengan pola yang benar: struktur response yang konsisten, resource transformer, pagination, dan dokumentasi.
Prinsip penting yang membedakan API yang baik: konsistensi. JSON yang terstruktur seragam membuat client mudah memakainya — dan dokumentasi yang jelas membuat tim lain tidak perlu membaca source code.
API memakai file routes/api.php yang prefix-nya /api:
Route::get('/posts', [Api\PostController::class, 'index']);
Route::get('/posts/{post}', [Api\PostController::class, 'show']);
Route::post('/posts', [Api\PostController::class, 'store'])
->middleware('auth:sanctum');Route di api.php otomatis diawali /api (diatur di RouteServiceProvider/bootstrap). Perhatikan perbedaannya dengan web.php: tanpa session & CSRF, memakai guard sanctum untuk autentikasi (episode 18). Cek dengan:
php artisan route:list --path=apiBuat controller khusus API:
php artisan make:controller Api/PostController --api<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Http\Resources\PostResource;
use App\Models\Post;
use Illuminate\Http\Request;
class PostController extends Controller
{
public function index()
{
return PostResource::collection(
Post::query()
->where('status', 'published')
->paginate(15)
);
}
public function show(Post $post)
{
return new PostResource($post);
}
public function store(Request $request)
{
$validated = $request->validate([
'title' => 'required|string|max:255',
'body' => 'required|string',
]);
$post = $request->user()->posts()->create($validated);
return (new PostResource($post))
->response()
->setStatusCode(201);
}
}Mengembalikan resource secara langsung membuat Laravel menghasilkan JSON dengan status code yang sesuai — 200 untuk koleksi/detail, 201 untuk resource yang baru dibuat.
API Resource mengontrol bentuk JSON — kolom apa saja yang boleh keluar, dan bagaimana formatnya:
php artisan make:resource 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,
'title' => $this->title,
'slug' => $this->slug,
'excerpt' => str($this->body)->limit(160),
'created_at' => $this->created_at->toIso8601String(),
'author' => new UserResource($this->whenLoaded('author')),
];
}
}Keuntungannya: kalian bisa menyembunyikan kolom sensitif (user_id internal, dsb.), menambahkan field turunan, dan menampilkan relasi hanya jika sudah di-eager load (whenLoaded). Client melihat API yang bersih, terlepas dari struktur tabel internal.
Untuk keseragaman error API, bungkus di helper atau gunakan pola:
return response()->json([
'message' => 'The given data was invalid.',
'errors' => $validator->errors(),
], 422);Konsisten = client tahu persis apa yang harus diparsing. Satu struktur untuk sukses, satu untuk error, satu untuk validasi.
paginate(15) pada koleksi resource menghasilkan struktur pagination otomatis:
{
"data": [ ... ],
"links": {
"first": "...",
"last": "...",
"prev": null,
"next": "..."
},
"meta": {
"current_page": 1,
"from": 1,
"last_page": 7,
"path": "...",
"per_page": 15,
"to": 15,
"total": 97
}
}Client memakai links.next untuk mengambil halaman berikutnya — tidak perlu parsing URL manual.
API berubah seiring waktu; client lama harus tetap jalan. Versioning adalah cara mengelola perubahan itu:
Route::prefix('v1')->group(function () {
Route::get('/posts', [Api\V1\PostController::class, 'index']);
});
Route::prefix('v2')->group(function () {
Route::get('/posts', [Api\V2\PostController::class, 'index']);
});Dengan URL /api/v1/posts dan /api/v2/posts, versi lama bisa di-deprecate bertahap tanpa memutus client lama. Aturan praktis: tambahkan versi baru ketika ada breaking change pada struktur response.
Dokumentasi yang hidup selalu up-to-date dengan kode. Scribe menghasilkan dokumentasi API interaktif dari atribut route dan controller:
composer require --dev knuckleswtf/scribe
php artisan scribe:installAnotasi di controller menjadi dokumentasi:
/**
* Daftar post yang sudah dipublikasikan.
*
* @queryParam page int Nomor halaman.
* @queryParam search string Filter judul.
*/
public function index()
{
// ...
}php artisan scribe:generateAlternatif lain yang umum adalah L5 Swagger (OpenAPI). Mana pun yang dipakai, dokumentasi otomatis lebih baik daripada dokumen yang ditulis tangan dan cepat basi.
Inti yang harus dibawa pulang:
routes/api.php (prefix /api), tanpa session/CSRF.whenLoaded.paginate() menghasilkan struktur links/meta siap pakai untuk client./api/v1, /api/v2) dan dokumentasikan dengan Scribe.Di episode 18 selanjutnya kita akan membahas API authentication dengan Sanctum — API tokens, HasApiTokens, authorization Bearer, token abilities/scope, dan perbandingan Sanctum vs Passport (OAuth). Sampai jumpa di episode 18!