Laravel · Bab 17
Praktik Produksi API
Kebiasaan sebelum API dirilis — feature test PHPUnit untuk mengunci perilaku, celah keamanan yang paling sering (mass assignment, rahasia .env, validasi, token di log), dan jawaban galat yang selalu JSON.
API dari Lab 8–12 sudah lengkap: baca, tulis, validasi, pagination, token, role, rate limit, dan CORS. Bab ini tidak punya lab di browser. Isinya kebiasaan yang membedakan API latihan dari API yang layak dipakai orang lain: tes otomatis, keamanan, dan galat yang rapi. Bagian berlabel Jobsheet Laptop dikerjakan di proyek Laravel di laptopmu sendiri. Contoh kode memakai proyek Smart Retail yang sudah kamu bangun (categories dan products), jadi bisa disalin ke proyekmu.
Feature Test dengan PHPUnit
Setiap kali kode diubah, kamu perlu yakin endpoint lama tidak rusak. Mencoba satu per satu di Klien API lambat dan mudah terlewat. Feature test mengirim request sungguhan ke aplikasi dan memeriksa jawabannya secara otomatis. Laravel sudah menyertakan PHPUnit; tesnya ada di folder tests/Feature.
Jobsheet Laptop: Feature Test API
- Buka
phpunit.xmldan pastikan dua baris berikut aktif (tidak berada di dalam komentar<!-- ... -->; di sebagian versi paket dasar Laravel 11 keduanya masih dikomentari). Dengan keduanya, tes memakai database SQLite di memori sehingga data aslimu tidak tersentuh:
<env name="DB_CONNECTION" value="sqlite"/>
<env name="DB_DATABASE" value=":memory:"/>
Syarat agar data aslimu benar-benar aman. (1) Ekstensi PHP
pdo_sqliteharus aktif. Di XAMPP/Laragon, pastikan barisextension=pdo_sqlitediphp.initidak diawali;; bila belum aktif,php artisan testberhenti dengan galatcould not find driver. (2) Jalankanphp artisan config:clearsebelumphp artisan test. Bila konfigurasi pernah di-cache (php artisan config:cache), nilai diphpunit.xmldiabaikan danRefreshDatabasebisa mengosongkan database MySQL-mu sendiri.
- Proyek Laravel 11 baru sudah membawa
tests/Feature/ExampleTest.phpyang mengharapkan/menjawab 200. Di Smart Retail,/mengarahkan (redirect) ke daftar produk, sehingga tes bawaan itu gagal danphp artisan testtidak akan lulus semua. Ubah isinya menjadi:
<?php
namespace Tests\Feature;
use Tests\TestCase;
class ExampleTest extends TestCase
{
public function test_the_application_returns_a_successful_response(): void
{
$this->get('/')->assertRedirect();
}
}
(Boleh juga menghapus berkas itu. Yang penting tes bawaan tidak lagi mengharapkan 200.)
3. Buat berkas tes: php artisan make:test ProductApiTest.
4. Isi tests/Feature/ProductApiTest.php:
<?php
namespace Tests\Feature;
use App\Models\Category;
use App\Models\Product;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class ProductApiTest extends TestCase
{
use RefreshDatabase;
public function test_daftar_produk_terbuka_untuk_umum(): void
{
$kategori = Category::create(['name' => 'Cold Beverages']);
Product::create(['category_id' => $kategori->id, 'product_code' => 'DR-001', 'name' => 'Es Teh Botol', 'price' => 5000, 'stock' => 20]);
$this->getJson('/api/v1/products')
->assertStatus(200)
->assertJsonPath('data.0.name', 'Es Teh Botol')
->assertJsonPath('meta.total', 1);
}
public function test_tambah_produk_tanpa_token_ditolak(): void
{
$this->postJson('/api/v1/products', ['name' => 'Teh Kotak'])
->assertStatus(401);
$this->assertDatabaseCount('products', 0);
}
public function test_tambah_produk_dengan_token_dan_data_kosong_menjawab_422(): void
{
User::factory()->create(['api_token' => 'token-uji']);
$this->withToken('token-uji')
->postJson('/api/v1/products', [])
->assertStatus(422)
->assertJsonValidationErrors(['name']);
}
}
- Jalankan
php artisan config:clear, laluphp artisan test. Setiap tes berjalan di database kosong karenaRefreshDatabase, sehingga urutan tes tidak saling memengaruhi.
getJson() dan postJson() otomatis mengirim Accept: application/json, sama seperti klien API sungguhan; karena itulah tes di atas mendapat 401 dan 422 berbentuk JSON. withToken() menambah header Authorization: Bearer <token>. User::factory() boleh mengisi api_token karena factory mengabaikan $fillable; endpoint publik tidak. Kebiasaan yang baik: setiap bug yang ditemukan diberi satu tes yang gagal dulu, baru diperbaiki.
Keamanan yang Paling Sering Terlewat
Mass assignment
Product::create($request->all()) menyimpan apa pun yang dikirim klien ke kolom yang ada di $fillable. Dua aturan:
- Simpan dengan
$request->validated()(hanya kolom yang lolos aturan validasi), bukan$request->all(). - Kolom yang menentukan hak atau identitas (
role,api_token,is_admin,user_idpemilik) tidak pernah masuk$fillable. Isi lewat properti di kode yang kamu kendalikan, seperti seeder di Lab 11–12.
Rahasia di .env
.env berisi APP_KEY, password database, dan kunci layanan lain. Berkas ini sudah tercantum di .gitignore dan tidak boleh ikut di-commit atau diunggah. Yang di-commit adalah .env.example tanpa nilai rahasia. Di server produksi, tetapkan APP_ENV=production dan APP_DEBUG=false. Dengan APP_DEBUG=true, jawaban galat API memuat exception, nama berkas, nomor baris, dan stack trace; dengan APP_DEBUG=false hanya message. Contoh jawaban 403 dari request dengan Accept: application/json:
{ "message": "This action is unauthorized." }
Validasi semua masukan
Body, parameter rute, dan query string sama-sama ditulis klien. Validasi ?search= dan ?sort= seperti di Lab 10, batasi panjang teks, dan pakai daftar putih untuk nilai yang menjadi nama kolom. Validasi juga melindungi database dari data rusak, bukan hanya dari penyerang.
Token tidak boleh masuk log
URL dicatat di log server, riwayat browser, dan alat pemantau. Karena itu token dikirim lewat header Authorization, bukan ?api_token= di alamat. Jangan pula mencatat $request->all() atau header mentah ke log. Simpan token dalam bentuk hash: Sanctum melakukannya otomatis, dan TokenGuard mendukungnya dengan 'hash' => true. Token di Lab 11–12 sengaja polos dan tetap hanya agar bisa dipelajari.
Galat yang Selalu JSON
Di bab 15 kamu membaca bahwa tanpa Accept: application/json, request API tanpa token diarahkan ke halaman login, dan 404 dijawab dengan halaman HTML. Klien yang lupa header itu jadi menerima HTML yang tidak bisa dibaca. Atur di bootstrap/app.php agar semua alamat api/* selalu dijawab JSON, dan beri pesan 404 yang tidak membocorkan nama model:
<?php
use Illuminate\Foundation\Application;
use Illuminate\Foundation\Configuration\Exceptions;
use Illuminate\Foundation\Configuration\Middleware;
use Illuminate\Http\Request;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
return Application::configure(basePath: dirname(__DIR__))
->withRouting(
web: __DIR__.'/../routes/web.php',
api: __DIR__.'/../routes/api.php',
commands: __DIR__.'/../routes/console.php',
health: '/up',
)
->withMiddleware(function (Middleware $middleware) {
//
})
->withExceptions(function (Exceptions $exceptions) {
$exceptions->shouldRenderJsonWhen(fn (Request $request) => $request->is('api/*') || $request->expectsJson());
$exceptions->render(function (NotFoundHttpException $e, Request $request) {
if ($request->is('api/*')) {
return response()->json(['message' => 'Data tidak ditemukan.'], 404);
}
});
})->create();
Catatan untuk proyek API murni (tanpa halaman web): pada Laravel 11 (v11.57 yang kami uji), ketika request ditolak auth dan jawabannya bukan JSON, Laravel mengalihkan tamu ke rute bernama login. Jika rute itu tidak ada, hasilnya galat 500 Route [login] not defined. alih-alih 401. shouldRenderJsonWhen di atas memilih jalur JSON lebih dulu untuk alamat api/*, sehingga rute login tidak diperlukan. Tanpa baris itu, klien yang lupa Accept: application/json akan melihat galat tersebut. Proyek Smart Retail sudah punya rute login dari Lab 7, jadi di proyekmu yang terlihat adalah pengalihan 302 (lihat bab 15).
Dengan shouldRenderJsonWhen, request ke api/* tanpa token dijawab 401 JSON dan alamat yang tidak ada dijawab 404 JSON, meskipun klien tidak mengirim Accept: application/json. Halaman web tetap dijawab HTML seperti biasa. Tanpa pengaturan ini, jawaban JSON untuk 401/403/404/422/429 hanya muncul bila klien mengirim Accept: application/json.
Ringkasan kode status yang sudah kamu pakai (kode galat berbentuk JSON bila klien mengirim Accept: application/json atau bila shouldRenderJsonWhen di atas dipasang):
| Kode | Arti | Contoh |
|---|---|---|
| 200 / 201 / 204 | berhasil / dibuat / berhasil tanpa isi | GET, POST, DELETE |
| 401 | belum dikenal (token tidak ada atau salah) | auth:api |
| 403 | dikenal, tetapi tidak boleh | Gate::authorize() |
| 404 | data atau alamat tidak ada | Route Model Binding |
| 422 | data tidak valid | FormRequest, $request->validate() |
| 429 | terlalu banyak request | throttle |
| 500 | galat di server | bug yang harus diperbaiki, bukan dijawab ke klien |
Daftar Periksa Sebelum Rilis
- Semua endpoint punya feature test untuk jalur sukses dan minimal satu jalur gagal (401, 403, 422;
getJson()/postJson()mengirimAccept: application/json). APP_DEBUG=false,.envtidak ada di repositori, danAPP_KEYproduksi berbeda dari kunci latihan.- Endpoint tulis memakai
auth, aksi berbahaya memakai Policy, dan login memakaithrottle. allowed_originsdiconfig/cors.phphanya berisi asal frontend yang sebenarnya.- Semua alamat
api/*menjawab JSON, termasuk saat galat.