Aplikasi web bisa berubah bebas karena HTML-nya ikut terdeploy bersama aplikasi; API tidak. Client mobile yang sudah terpasang di jutaan ponsel tidak ikut ter-update begitu kita merilis kode baru, begitu pula integrasi milik mitra yang dibangun di atas kontrak kita enam bulan lalu. Versioning adalah janji bahwa kontrak lama tetap terjaga selama periode yang disepakati, sementara perubahan besar diberi rumah baru. Chapter ini mempraktikkan pola URI versioning /api/v1 dan /api/v2, lalu melengkapinya dengan strategi deprecation yang jujur — header Sunset, dokumentasi, dan kejujuran soal biaya menjaga dua versi berdampingan.
Kapan Versi Harus Naik #
Kenaikan versi bukan respons otomatis atas setiap perubahan — hanya perubahan yang melanggar (breaking) yang membutuhkannya. Taksonomi berikut menjadi alat pengambil keputusan:
| Perubahan | Kategori |
| | |
| Menambah field opsional di response | Tidak melanggar |
| Menambah endpoint baru | Tidak melanggar |
| Menambah parameter opsional | Tidak melanggar |
| Mengganti nama field (title → judul) | Melanggar |
| Mengubah tipe field (string → object) | Melanggar |
| Menghapus field dari response | Melanggar |
| Mengubah status code sukses (200 → 201) | Melanggar |
| Memperketat validasi input | Melanggar (client yang tadinya lolos bisa ditolak) |
Aturan praktisnya: bila client yang ditulis kemarin bisa rusak besok karena perubahan ini, naikkan versinya. Aturan kedua yang sama pentingnya: perubahan tidak-melanggar dirilis di versi yang sama tanpa rasa bersalah — menahan diri dari kenaikan versi adalah bentuk kepedulian pada konsumen.
Route File per Versi #
Dua versi berdampingan membuat satu file route membesar dengan cepat, jadi kita pecah per versi. File routes/api.php berubah menjadi manifes tipis yang memuat kedua file:
<?php
// routes/api.php
use Illuminate\Support\Facades\Route;
Route::prefix('v1')->group(base_path('routes/api_v1.php'));
Route::prefix('v2')->group(base_path('routes/api_v2.php'));Method group() menerima path file — semua route di dalam routes/api_v1.php otomatis mendapat prefix v1, dan api_v2.php mendapat v2, pola yang persis dipakai framework untuk memuat file route sebagaimana dibahas di dokumentasi routing
. Controller versi baru pindah ke namespace sendiri, App\Http\Controllers\Api\V2, agar evolusinya tidak menumpuk di controller lama:
<?php
// routes/api_v2.php
use App\Http\Controllers\Api\V2\PostController;
use Illuminate\Support\Facades\Route;
Route::apiResource('posts', PostController::class)
->only(['index', 'show'])
->names('v2.posts');Perhatikan ->names('v2.posts') — tanpanya nama route posts.index bentrok antar versi dan nama yang terdaftar terakhir diam-diam menimpa yang pertama. Struktur hasilnya terlihat utuh dari route:list:
Output yang Dihasilkan:
$ php artisan route:list --path=api
POST api/v1/auth/token ........................... routes/api_v1.php:16
GET|HEAD api/v1/me ................................... routes/api_v1.php:40
GET|HEAD api/v1/posts .............. posts.index › Api\PostController@index
POST api/v1/posts .............. posts.store › Api\PostController@store
GET|HEAD api/v1/posts-simpel ......................... routes/api_v1.php:58
GET|HEAD api/v1/posts/{post} ......... posts.show › Api\PostController@show
PUT api/v1/posts/{post} .................... Api\PostController@update
DELETE api/v1/posts/{post} ................... Api\PostController@destroy
POST api/v1/posts/{post}/publish ................. routes/api_v1.php:68
GET|HEAD api/v2/posts ........ v2.posts.index › Api\V2\PostController@index
GET|HEAD api/v2/posts/{post} ... v2.posts.show › Api\V2\PostController@show
Showing [11] routesv2: Perubahan yang Melanggar #
Apa yang berubah di v2? Kasus nyatanya: show di v1 masih mengembalikan model mentah — nama kolom database (title, content, user_id) bocor ke kontrak publik. v2 memperbaikinya dengan field berbahasa kontrak (judul, isi) sekaligus menambahkan ringkasan. Kedua versi menjawab post yang sama dengan bentuk yang berbeda:
Output yang Dihasilkan:
### GET /api/v1/posts/1 → HTTP 200
{
"id": 1,
"user_id": 1,
"title": "Belajar Versioning API",
"content": "Konten post versi pertama.",
"status": "published",
"published_at": "2026-01-02T09:00:00.000000Z",
"created_at": "2026-01-02T09:00:00.000000Z",
"updated_at": "2026-01-02T09:00:00.000000Z"
}
### GET /api/v2/posts/1 → HTTP 200
{
"data": {
"id": 1,
"judul": "Belajar Versioning API",
"isi": "Konten post versi pertama.",
"ringkasan": "Konten post versi pe..."
}
}Implementasi v2 di controller-nya sendiri:
<?php
// app/Http/Controllers/Api/V2/PostController.php
namespace App\Http\Controllers\Api\V2;
use App\Http\Controllers\Controller;
use App\Models\Post;
use Illuminate\Http\JsonResponse;
use Illuminate\Support\Str;
class PostController extends Controller
{
public function show(Post $post): JsonResponse
{
return response()->json([
'data' => [
'id' => $post->id,
'judul' => $post->title,
'isi' => $post->content,
'ringkasan' => Str::limit($post->content, 20),
],
]);
}
}Deprecation yang Jujur #
Menjaga v1 selamanya bukan strategi — itu utang. Deprecation yang jujur berarti client tahu bahwa endpoint akan mati dan kapan. Praktiknya: pasang header Deprecation dan Sunset pada endpoint yang dijadwalkan, sehingga bisa dipantau otomatis:
<?php
// app/Http/Middleware/MarkDeprecated.php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
class MarkDeprecated
{
public function handle(Request $request, Closure $next, string $version, string $sunsetDate): Response
{
return $next($request)->withHeaders([
'Deprecation' => 'true',
'Sunset' => gmdate('D, d M Y H:i:s', strtotime($sunsetDate.' 23:59:59 GMT')).' GMT',
'Link' => '</docs/api>; rel="deprecation"',
]);
}
}Middleware ini dipasang per endpoint dengan dua parameter — versi dan tanggal sunset:
<?php
// routes/api_v1.php
Route::get('/posts/{post}', [PostController::class, 'show'])
->middleware('deprecated:v1,2025-12-31')
->name('posts.show');Header yang dikembalikan sekarang bisa dijadikan bahan monitoring — tim client bisa memasang peringatan di log mereka setiap kali Sunset terlihat:
Output yang Dihasilkan:
### GET /api/v1/posts/1 → header response
Deprecation: true
Sunset: Wed, 31 Dec 2025 23:59:59 GMT
Link: </docs/api>; rel="deprecation"Biaya Dua Versi dan Alternatifnya #
Setiap bug fix kini harus ditulis dua kali — di v1 dan v2 — dan setiap data baru harus tetap masuk ke kontrak lama. Karena itu versi paralel selalu punya tanggal mati sejak hari pertama, dan pertimbangkan strategi alternatif sebelum menaikkan versi: beberapa API memakai header-based versioning, misalnya client mengirim Accept-Version: 2 dan middleware memilih handler yang sesuai. Pendekatan ini menjaga URL tetap stabil, tetapi lebih sulit di-debug (perubahan tidak terlihat di URL), tidak bisa diuji lewat browser, dan tetap membutuhkan logika pemilihan versi di server — kebanyakan tim akhirnya kembali ke prefix URI yang eksplisit. Untuk API Course ini, prefix URI adalah pilihan utamanya karena terlihat, teruji, dan bisa dipaginasi di log tanpa alat tambahan.
Dengan dua versi yang hidup teratur dan v1 yang terjadwal mati, pertanyaan operasional berikutnya adalah perlindungan dari penyalahgunaan: client yang bersemangat mengirim ribuan request per menit bisa melumpuhkan kedua versi sekaligus. Rate Limiting adalah chapter penutup masalah itu.