1. LATAR BELAKANG
Setelah
mempelajari file upload dan storage management, API kita semakin
lengkap fiturnya. Namun, API yang profesional harus bisa berkembang
tanpa merusak aplikasi client yang sudah ada. Bayangkan jika kita
mengubah struktur response endpoint /api/posts - semua mobile app yang menggunakan API kita akan error! Di sinilah API Versioning menjadi sangat penting.
Selain itu, API publik harus dilindungi dari penyalahgunaan. Bayangkan jika ada pengguna yang melakukan request 1000 kali per detik - server kita bisa down dan biaya membengkak. Di sinilah Rate Limiting berperan untuk membatasi jumlah request dalam periode waktu tertentu.
Pada hari keempat minggu ini, saya mempelajari dua konsep krusial untuk API production:
1. API Versioning
- Mengelola perubahan API tanpa merusak client lama
- Strategi versioning (URL, Header, Accept Header)
- Backward compatibility
2. Rate Limiting
- Melindungi server dari abuse
- Membatasi request per user/IP
- Custom rate limiter per endpoint
- Response handling saat limit tercapai
Dengan kedua fitur ini, API kita siap digunakan oleh banyak client dengan skala besar.
2. ALAT DAN BAHAN
2.1 Perangkat Lunak
- Laravel 11 - Project API dari hari sebelumnya
- Redis/Memcached - Untuk rate limiting yang lebih baik (opsional)
- Postman/Thunder Client - Untuk testing rate limiting
- Visual Studio Code - Editor kode
- Git Bash/Terminal - Untuk menjalankan perintah Artisan
2.2 Perangkat Keras
- Laptop dengan spesifikasi standar
3. PEMBAHASAN
3.1 API Versioning - Konsep Dasar
API Versioning adalah praktik membuat beberapa versi API secara bersamaan sehingga client bisa memilih versi mana yang ingin mereka gunakan.
3.1.1 Mengapa Perlu Versioning?
🚫 TANPA VERSIONING: V1 (Januari) : /api/users → { "id": 1, "name": "John", "email": "john@mail.com" } V2 (Maret) : /api/users → { "id": 1, "full_name": "John Doe", "email_address": "john@mail.com" } Hasilnya: Semua app yang menggunakan API V1 akan error karena struktur response berubah!
✅ DENGAN VERSIONING: V1 : /api/v1/users → { "id": 1, "name": "John", "email": "john@mail.com" } V2 : /api/v2/users → { "id": 1, "full_name": "John Doe", "email_address": "john@mail.com" } Hasilnya: App lama tetap pakai V1, app baru bisa pakai V2. Semua happy!
3.1.2 Strategi Versioning
Ada 3 strategi umum untuk API versioning:
| Strategi | Cara | Contoh | Kelebihan | Kekurangan |
|---|---|---|---|---|
| URL Path | Versi di path URL | /api/v1/posts | Sederhana, jelas | Memperpanjang URL |
| Query Parameter | Versi di query string | /api/posts?version=1 | URL tetap bersih | Mudah lupa |
| Header | Versi di Accept header | Accept: application/vnd.api.v1+json | Paling RESTful | Lebih kompleks |
3.2 Versioning dengan URL Path (Paling Umum)
3.2.1 Struktur Folder untuk Versioning
app/ ├── Http/ │ ├── Controllers/ │ │ ├── V1/ │ │ │ ├── PostController.php │ │ │ ├── UserController.php │ │ │ └── AuthController.php │ │ └── V2/ │ │ ├── PostController.php │ │ ├── UserController.php │ │ └── AuthController.php │ ├── Requests/ │ │ ├── V1/ │ │ │ └── PostRequest.php │ │ └── V2/ │ │ └── PostRequest.php │ └── Resources/ │ ├── V1/ │ │ ├── PostResource.php │ │ └── UserResource.php │ └── V2/ │ ├── PostResource.php │ └── UserResource.php
3.2.2 Membuat Controller dengan Namespace
# Buat controller untuk V1 php artisan make:controller V1/PostController --api --model=Post php artisan make:controller V1/UserController --api --model=User # Buat controller untuk V2 php artisan make:controller V2/PostController --api --model=Post php artisan make:controller V2/UserController --api --model=User
3.2.3 Routing dengan Prefix Versi
<?php // File: routes/api.php use Illuminate\Support\Facades\Route; // ===== API VERSION 1 ===== Route::prefix('v1')->group(function () { // Public endpoints V1 Route::get('/posts', [App\Http\Controllers\V1\PostController::class, 'index']); Route::get('/posts/{post}', [App\Http\Controllers\V1\PostController::class, 'show']); // Auth endpoints V1 Route::prefix('auth')->group(function () { Route::post('/login', [App\Http\Controllers\V1\AuthController::class, 'login']); Route::post('/register', [App\Http\Controllers\V1\AuthController::class, 'register']); }); // Protected endpoints V1 Route::middleware('auth:sanctum')->group(function () { Route::apiResource('posts', App\Http\Controllers\V1\PostController::class) ->except(['index', 'show']); Route::get('/user', [App\Http\Controllers\V1\UserController::class, 'profile']); }); }); // ===== API VERSION 2 ===== Route::prefix('v2')->group(function () { // Public endpoints V2 (dengan fitur baru) Route::get('/posts', [App\Http\Controllers\V2\PostController::class, 'index']); Route::get('/posts/{post}', [App\Http\Controllers\V2\PostController::class, 'show']); Route::get('/posts/{post}/comments', [App\Http\Controllers\V2\PostController::class, 'comments']); // Auth endpoints V2 (dengan fitur refresh token) Route::prefix('auth')->group(function () { Route::post('/login', [App\Http\Controllers\V2\AuthController::class, 'login']); Route::post('/register', [App\Http\Controllers\V2\AuthController::class, 'register']); Route::post('/refresh', [App\Http\Controllers\V2\AuthController::class, 'refresh']); }); // Protected endpoints V2 Route::middleware('auth:sanctum')->group(function () { Route::apiResource('posts', App\Http\Controllers\V2\PostController::class) ->except(['index', 'show']); Route::get('/user', [App\Http\Controllers\V2\UserController::class, 'profile']); Route::put('/user/preferences', [App\Http\Controllers\V2\UserController::class, 'preferences']); }); });
3.2.4 Controller V1 - Versi Lama
<?php // File: app/Http/Controllers/V1/PostController.php namespace App\Http\Controllers\V1; use App\Http\Controllers\Controller; use App\Models\Post; use App\Http\Resources\V1\PostResource; use App\Http\Requests\V1\PostRequest; use App\Traits\ApiResponseTrait; class PostController extends Controller { use ApiResponseTrait; /** * GET /api/v1/posts * Versi 1: Hanya data dasar */ public function index() { $posts = Post::with('author:id,name') ->latest() ->paginate(15); return PostResource::collection($posts); } /** * GET /api/v1/posts/{post} * Versi 1: Detail sederhana */ public function show(Post $post) { $post->load('author:id,name'); return new PostResource($post); } /** * POST /api/v1/posts * Versi 1: Create dengan field minimal */ public function store(PostRequest $request) { $post = Post::create([ 'title' => $request->title, 'content' => $request->content, 'category_id' => $request->category_id, 'user_id' => auth()->id() ]); return new PostResource($post); } }
3.2.5 Controller V2 - Versi Baru dengan Fitur Tambahan
<?php // File: app/Http/Controllers/V2/PostController.php namespace App\Http\Controllers\V2; use App\Http\Controllers\Controller; use App\Models\Post; use App\Http\Resources\V2\PostResource; use App\Http\Requests\V2\PostRequest; use App\Traits\ApiResponseTrait; use Illuminate\Http\Request; class PostController extends Controller { use ApiResponseTrait; /** * GET /api/v2/posts * Versi 2: Dengan fitur filtering, sorting, dan metadata lengkap */ public function index(Request $request) { $query = Post::with(['author:id,name,email', 'category', 'tags']) ->withCount(['comments', 'likes']); // Filter by category if ($request->has('category')) { $query->whereHas('category', fn($q) => $q->where('slug', $request->category)); } // Filter by tags if ($request->has('tags')) { $tagIds = explode(',', $request->tags); $query->whereHas('tags', fn($q) => $q->whereIn('tags.id', $tagIds)); } // Search if ($request->has('search')) { $query->where(fn($q) => $q->where('title', 'LIKE', "%{$request->search}%") ->orWhere('content', 'LIKE', "%{$request->search}%")); } // Sorting multi kolom if ($request->has('sort')) { foreach (explode(',', $request->sort) as $sort) { $direction = str_starts_with($sort, '-') ? 'desc' : 'asc'; $field = ltrim($sort, '-'); $query->orderBy($field, $direction); } } $posts = $query->paginate($request->get('per_page', 15)); return PostResource::collection($posts); } /** * GET /api/v2/posts/{post} * Versi 2: Detail lengkap dengan semua relasi */ public function show(Post $post) { $post->load([ 'author:id,name,email,role', 'category', 'tags', 'comments' => fn($q) => $q->latest()->limit(10), 'comments.user:id,name' ])->loadCount(['comments', 'likes']); // Increment views $post->increment('views'); return new PostResource($post); } /** * GET /api/v2/posts/{post}/comments * Fitur baru di V2: Ambil semua komentar post */ public function comments(Post $post, Request $request) { $comments = $post->comments() ->with('user:id,name') ->latest() ->paginate($request->get('per_page', 20)); return response()->json([ 'data' => $comments, 'post_title' => $post->title ]); } /** * POST /api/v2/posts * Versi 2: Create dengan field lengkap + tags */ public function store(PostRequest $request) { $post = Post::create([ 'title' => $request->title, 'content' => $request->content, 'category_id' => $request->category_id, 'user_id' => auth()->id(), 'meta_description' => $request->meta_description, 'featured_image' => $request->featured_image, 'is_published' => $request->is_published ?? false, 'published_at' => $request->is_published ? now() : null ]); // Attach tags jika ada if ($request->has('tags')) { $post->tags()->sync($request->tags); } $post->load(['category', 'tags']); return new PostResource($post); } }
3.2.6 Resource V1 vs V2
Resource V1 - Sederhana:
<?php // File: app/Http/Resources/V1/PostResource.php namespace App\Http\Resources\V1; 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, 'content' => $this->content, 'author' => [ 'id' => $this->author->id, 'name' => $this->author->name ], 'created_at' => $this->created_at->format('Y-m-d') ]; } }
Resource V2 - Lengkap:
<?php // File: app/Http/Resources/V2/PostResource.php namespace App\Http\Resources\V2; 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' => substr($this->content, 0, 150) . '...', 'content' => $this->content, 'featured_image' => $this->featured_image ? asset('storage/' . $this->featured_image) : null, 'views' => $this->views, 'meta_description' => $this->meta_description, 'author' => [ 'id' => $this->author->id, 'name' => $this->author->name, 'avatar' => $this->author->profile?->avatar_url ], 'category' => [ 'id' => $this->category->id, 'name' => $this->category->name, 'slug' => $this->category->slug ], 'tags' => $this->whenLoaded('tags', function() { return $this->tags->map(fn($tag) => [ 'id' => $tag->id, 'name' => $tag->name, 'slug' => $tag->slug ]); }), 'stats' => [ 'comments_count' => $this->comments_count ?? 0, 'likes_count' => $this->likes_count ?? 0 ], 'published_at' => $this->published_at?->format('Y-m-d H:i:s'), 'created_at' => $this->created_at->format('Y-m-d H:i:s'), 'created_ago' => $this->created_at->diffForHumans(), 'links' => [ 'self' => route('api.v2.posts.show', $this->id), 'comments' => route('api.v2.posts.comments', $this->id) ] ]; } }
3.3 Versioning dengan Accept Header (RESTful)
Untuk pendekatan yang lebih RESTful, kita bisa menggunakan Accept Header.
3.3.1 Middleware untuk Versioning via Header
php artisan make:middleware ApiVersion<?php // File: app/Http/Middleware/ApiVersion.php namespace App\Http\Middleware; use Closure; use Illuminate\Http\Request; class ApiVersion { /** * Handle an incoming request. */ public function handle(Request $request, Closure $next, $version = 'v1') { // Cek Accept header $acceptHeader = $request->header('Accept'); if ($acceptHeader) { // Format: application/vnd.api.v1+json preg_match('/application\/vnd\.api\.(v\d+)\+json/', $acceptHeader, $matches); if (isset($matches[1])) { $version = $matches[1]; } } // Set versi di request untuk digunakan nanti $request->attributes->set('api_version', $version); // Ubah namespace controller berdasarkan versi $this->setControllerNamespace($version); return $next($request); } /** * Set controller namespace based on version */ protected function setControllerNamespace($version) { $version = strtoupper($version); app('config')->set('api.controller_namespace', "App\\Http\\Controllers\\{$version}"); } }
3.3.2 Daftarkan Middleware
<?php // File: app/Http/Kernel.php protected $routeMiddleware = [ // ... 'api.version' => \App\Http\Middleware\ApiVersion::class, ];
3.3.3 Routing dengan Middleware Version
<?php // File: routes/api.php use Illuminate\Support\Facades\Route; Route::middleware(['api.version'])->group(function () { // Posts routes - akan mengarah ke controller sesuai versi Route::get('/posts', function (Request $request) { $version = $request->attributes->get('api_version', 'v1'); $controller = app('config')->get('api.controller_namespace') . '\PostController'; return app($controller)->index($request); }); Route::get('/posts/{post}', function (Request $request, $post) { $controller = app('config')->get('api.controller_namespace') . '\PostController'; return app($controller)->show($request, $post); }); });
3.3.4 Penggunaan di Client
# Request dengan versi 1 GET /api/posts Accept: application/vnd.api.v1+json # Request dengan versi 2 GET /api/posts Accept: application/vnd.api.v2+json
3.4 Rate Limiting - Konsep Dasar
Rate limiting membatasi jumlah request yang bisa dilakukan client dalam periode waktu tertentu.
3.4.1 Mengapa Perlu Rate Limiting?
- Mencegah DDoS - Serangan yang membanjiri server
- Fair Usage - Semua client mendapat jatah yang adil
- Proteksi dari Bug - Mencegah infinite loop di client
- Monetisasi - Membuat tier berbayar dengan limit lebih tinggi
- Stabilitas Server - Menjaga performa untuk semua pengguna
3.4.2 Rate Limiting Bawaan Laravel
Laravel sudah menyediakan middleware throttle untuk rate limiting.
// Format: throttle:max_attempts,decay_minutes Route::middleware('throttle:60,1')->group(function () { Route::get('/posts', [PostController::class, 'index']); });
3.5 Implementasi Rate Limiting
3.5.1 Rate Limiting Global untuk API
<?php // File: app/Http/Kernel.php protected $middlewareGroups = [ 'api' => [ 'throttle:60,1', // 60 request per menit untuk semua endpoint API \Illuminate\Routing\Middleware\SubstituteBindings::class, ], ];
3.5.2 Rate Limiting Berbeda per Endpoint
<?php // File: routes/api.php use Illuminate\Support\Facades\Route; // Endpoint publik - limit rendah Route::middleware('throttle:30,1')->group(function () { Route::get('/posts', [PostController::class, 'index']); Route::get('/posts/{post}', [PostController::class, 'show']); }); // Endpoint yang berat - limit sangat rendah Route::middleware('throttle:10,1')->group(function () { Route::get('/posts/{post}/export', [PostController::class, 'export']); Route::get('/reports/generate', [ReportController::class, 'generate']); }); // Endpoint authenticated - limit lebih tinggi Route::middleware(['auth:sanctum', 'throttle:100,1'])->group(function () { Route::apiResource('posts', PostController::class)->except(['index', 'show']); Route::post('/posts/{post}/comments', [CommentController::class, 'store']); }); // Endpoint khusus admin - limit sangat tinggi Route::middleware(['auth:sanctum', 'role:admin', 'throttle:500,1'])->group(function () { Route::get('/admin/stats', [AdminController::class, 'stats']); Route::post('/admin/bulk-import', [AdminController::class, 'import']); });
3.5.3 Rate Limiting Berdasarkan User/IP
// throttle:60,1|api -> 60 untuk authenticated, 30 untuk guest Route::middleware('throttle:60,1|30,1')->group(function () { Route::get('/posts', [PostController::class, 'index']); });
3.5.4 Custom Rate Limiter dengan Redis
Untuk kontrol lebih, kita bisa membuat rate limiter kustom.
<?php // File: app/Providers/AppServiceProvider.php use Illuminate\Cache\RateLimiting\Limit; use Illuminate\Http\Request; use Illuminate\Support\Facades\RateLimiter; public function boot() { // Rate limiter untuk API umum RateLimiter::for('api', function (Request $request) { return Limit::perMinute(60)->by($request->user()?->id ?: $request->ip()); }); // Rate limiter untuk upload RateLimiter::for('uploads', function (Request $request) { return $request->user() ? Limit::perMinute(10)->by($request->user()->id) : Limit::perMinute(3)->by($request->ip()); }); // Rate limiter untuk login (mencegah brute force) RateLimiter::for('login', function (Request $request) { return [ Limit::perMinute(5)->by($request->input('email') ?: $request->ip()), Limit::perMinute(100)->by($request->ip()), ]; }); // Rate limiter berdasarkan role RateLimiter::for('posts', function (Request $request) { if ($request->user()?->role === 'admin') { return Limit::perMinute(200)->by($request->user()->id); } if ($request->user()) { return Limit::perMinute(50)->by($request->user()->id); } return Limit::perMinute(10)->by($request->ip()); }); // Rate limiter dengan dynamic response RateLimiter::for('critical', function (Request $request) { return Limit::perMinute(5)->by($request->user()?->id ?: $request->ip()) ->response(function (Request $request, array $headers) { return response()->json([ 'success' => false, 'message' => 'Terlalu banyak percobaan. Silakan tunggu ' . $headers['Retry-After'] . ' detik.', 'retry_after' => $headers['Retry-After'] ], 429); }); }); }
3.5.5 Menggunakan Custom Rate Limiter di Routes
<?php // File: routes/api.php // Menggunakan rate limiter 'api' Route::middleware(['throttle:api'])->group(function () { Route::get('/posts', [PostController::class, 'index']); }); // Menggunakan rate limiter 'uploads' Route::middleware(['auth:sanctum', 'throttle:uploads'])->group(function () { Route::post('/posts/{post}/images', [PostController::class, 'uploadImages']); }); // Menggunakan rate limiter 'login' untuk endpoint login Route::post('/auth/login', [AuthController::class, 'login']) ->middleware('throttle:login'); // Menggunakan rate limiter 'posts' yang dinamis Route::middleware(['throttle:posts'])->group(function () { Route::post('/posts', [PostController::class, 'store']); Route::put('/posts/{post}', [PostController::class, 'update']); Route::delete('/posts/{post}', [PostController::class, 'destroy']); });
3.6 Advanced Rate Limiting
3.6.1 Rate Limiting per Endpoint dengan Dynamic Limits
<?php // File: app/Providers/AppServiceProvider.php use Illuminate\Support\Facades\RateLimiter; RateLimiter::for('dynamic', function (Request $request) { $user = $request->user(); if (!$user) { return Limit::perMinute(10)->by($request->ip()); } // Different limits based on user subscription switch ($user->subscription_tier) { case 'premium': return Limit::perMinute(500)->by($user->id); case 'basic': return Limit::perMinute(100)->by($user->id); default: return Limit::perMinute(30)->by($user->id); } });
3.6.2 Rate Limiting dengan Burst Capacity
use Illuminate\Cache\RateLimiting\Limit; RateLimiter::for('burst', function (Request $request) { return [ // Burst: 10 request per 10 detik Limit::perMinutes(10, 10)->by($request->ip()), // Sustained: 100 request per jam Limit::perHour(100)->by($request->ip()), ]; });
3.6.3 Rate Limiting untuk API Keys
RateLimiter::for('api_key', function (Request $request) { $apiKey = $request->header('X-API-Key'); if (!$apiKey) { return Limit::perMinute(5)->by($request->ip()); } // Cari API key di database $key = ApiKey::where('key', $apiKey)->first(); if ($key && $key->is_active) { return Limit::perMinute($key->rate_limit)->by($apiKey); } return Limit::perMinute(5)->by($request->ip()); });
3.7 Response Handling Saat Rate Limit Tercapai
3.7.1 Response Default Laravel
Saat rate limit tercapai, Laravel mengembalikan response:
{ "message": "Too Many Attempts." }
Status code: 429 Too Many Requests
Headers yang dikirim:
X-RateLimit-Limit: 60 X-RateLimit-Remaining: 0 Retry-After: 58
3.7.2 Custom Response untuk Rate Limit
<?php // File: app/Exceptions/Handler.php use Illuminate\Http\Exceptions\ThrottleRequestsException; public function render($request, Throwable $exception) { if ($exception instanceof ThrottleRequestsException) { return response()->json([ 'success' => false, 'message' => 'Terlalu banyak permintaan. Silakan tunggu beberapa saat.', 'limit' => $exception->getHeaders()['X-RateLimit-Limit'] ?? null, 'remaining' => $exception->getHeaders()['X-RateLimit-Remaining'] ?? null, 'retry_after' => $exception->getHeaders()['Retry-After'] ?? null ], 429); } return parent::render($request, $exception); }
3.7.3 Memberi Informasi Rate Limit di Response
<?php // File: app/Http/Middleware/AddRateLimitHeaders.php namespace App\Http\Middleware; use Closure; class AddRateLimitHeaders { public function handle($request, Closure $next) { $response = $next($request); if (method_exists($response, 'header') && $request->route()?->middleware()) { // Hitung sisa limit (ini contoh sederhana) $executed = RateLimiter::attempts('api'); $limit = 60; $response->header('X-RateLimit-Limit', $limit); $response->header('X-RateLimit-Remaining', max(0, $limit - $executed)); } return $response; } }
3.8 Testing Rate Limiting
3.8.1 Testing dengan PHPUnit
<?php // File: tests/Feature/RateLimitTest.php namespace Tests\Feature; use Tests\TestCase; use App\Models\User; use Illuminate\Support\Facades\RateLimiter; class RateLimitTest extends TestCase { public function test_rate_limit_for_guests() { // Lakukan 31 request (limit 30 per menit) for ($i = 0; $i < 31; $i++) { $response = $this->getJson('/api/posts'); if ($i < 30) { $response->assertStatus(200); } else { $response->assertStatus(429); $response->assertJson([ 'message' => 'Too Many Attempts.' ]); } } } public function test_rate_limit_for_authenticated_users() { $user = User::factory()->create(); $token = $user->createToken('test')->plainTextToken; // Lakukan 101 request (limit 100 per menit untuk auth) for ($i = 0; $i < 101; $i++) { $response = $this->withHeaders([ 'Authorization' => 'Bearer ' . $token ]) ->getJson('/api/posts'); if ($i < 100) { $response->assertStatus(200); } else { $response->assertStatus(429); } } } }
3.8.2 Testing dengan Postman
Test Script di Postman:
// Tests tab pm.test("Rate limit headers exist", function () { pm.expect(pm.response.headers.get('X-RateLimit-Limit')).to.exist; pm.expect(pm.response.headers.get('X-RateLimit-Remaining')).to.exist; }); pm.test("Status code is 200 or 429", function () { pm.expect(pm.response.code).to.be.oneOf([200, 429]); }); // Jika kena rate limit if (pm.response.code === 429) { let retryAfter = pm.response.headers.get('Retry-After'); console.log('Rate limited. Retry after ' + retryAfter + ' seconds'); // Set delay untuk request berikutnya setTimeout(function() { console.log('Sending request again...'); // Logic untuk repeat request }, retryAfter * 1000); }
3.8.3 Testing dengan Command Line
# Test rate limit dengan curl loop for i in {1..100}; do curl -s -o /dev/null -w "%{http_code}\n" http://localhost:8000/api/posts done # Dengan header for i in {1..100}; do curl -s -o /dev/null -w "%{http_code}\n" \ -H "Authorization: Bearer your-token" \ http://localhost:8000/api/posts done
3.9 Best Practices API Versioning & Rate Limiting
3.9.1 Versioning Best Practices
- Rencanakan dari awal - Meskipun V1 sederhana, siapkan struktur untuk V2
- Dokumentasi jelas - Beri tahu client versi mana yang akan di-deprecate
- Support minimal 2 versi - Beri waktu client migrasi (6-12 bulan)
- Semantic Versioning - Gunakan v1, v2, bukan v1.0.1 (untuk API)
- Backward compatibility - Jangan hapus field di versi yang sama
3.9.2 Rate Limiting Best Practices
- Beri informasi jelas - Sertakan header rate limit di response
- Limit berbeda untuk tiap endpoint - Login lebih ketat dari read-only
- Burst vs sustained - Izinkan burst pendek, batasi rata-rata
- Monitor dan adjust - Lihat pattern usage, sesuaikan limit
- Graceful degradation - Beri response yang informatif saat kena limit
3.9.3 Contoh Kebijakan Rate Limiting Realistis
| Endpoint | Guest | User Biasa | Premium | Admin |
|---|---|---|---|---|
| GET /api/posts | 30/min | 60/min | 120/min | 300/min |
| POST /api/posts | - | 10/min | 30/min | 100/min |
| POST /api/auth/login | 5/min | - | - | - |
| POST /api/upload | - | 3/min | 10/min | 50/min |
| GET /api/admin/* | - | - | - | 200/min |
3.10 Kendala dan Solusi
| Kendala | Solusi |
|---|---|
| Lupa menambahkan versioning di awal | Refactor routing, buat prefix v1 untuk semua endpoint existing |
| Cache rate limit tidak bekerja di load balancer | Gunakan Redis sebagai central cache, bukan file |
| Rate limit terlalu ketat | Monitoring, analisis usage pattern, adjust limit |
| User mengeluh kena limit padahal tidak wajar | Cek kemungkinan bug di client (infinite loop), beri penjelasan |
| Versioning bikin kode duplicate | Gunakan traits untuk shared logic, inheritance untuk controller |
| Testing rate limit lambat | Gunakan Cache::shouldReceive() untuk mock di test |
| Lupa deprecate versi lama | Buat schedule deprecation, kirim warning ke developer |
4. KESIMPULAN
API Versioning dan Rate Limiting adalah dua fitur yang membuat API kita siap untuk skala produksi dengan banyak client.
- API Versioning
- URL Path Versioning: Sederhana, jelas, paling umum digunakan
- Header Versioning: RESTful, URL bersih, lebih kompleks
- Struktur Folder: Pisahkan Controller, Request, Resource per versi
- Backward Compatibility: Client lama tetap bisa pakai versi lama
- Rate Limiting
- Global Limiting: Proteksi dasar untuk semua endpoint
- Per-Endpoint Limiting: Limit berbeda untuk tiap fungsionalitas
- User-Based Limiting: Authenticated user dapat limit lebih tinggi
- Custom Rate Limiter: Redis, dynamic based on role/tier
- Response Headers: Informasi limit untuk client
Manfaat yang Didapat:
- API bisa berkembang tanpa merusak client existing
- Server terlindungi dari abuse dan DDoS
- Fair usage untuk semua pengguna
- Bisa monetisasi dengan tier berbeda
- Client mendapat informasi jelas tentang limit mereka
Dengan kedua fitur ini, API kita sekarang benar-benar production-ready. Pada hari kelima, kita akan mempelajari Testing REST API untuk memastikan semua fitur bekerja dengan baik dan tidak ada regression.
5. DAFTAR PUSTAKA
- Laravel Official Documentation. (n.d.). Laravel 12.x Documentation - API Versioning. https://laravel.com/docs/12.x/api-versioning
- Laravel Official Documentation. (n.d.). Laravel 12.x Documentation - Rate Limiting. https://laravel.com/docs/12.x/rate-limiting
- Laravel Official Documentation. (n.d.). Laravel 12.x Documentation - Middleware. https://laravel.com/docs/12.x/middleware
- Microsoft REST API Guidelines. (n.d.). Versioning. https://github.com/microsoft/api-guidelines/blob/vNext/Guidelines.md#12-versioning

0 Komentar