REST CRUD Lengkap: Store, Update, Destroy via API
Lengkapi API dengan operasi tulis plus validasi dan status code tepat.
Arsitektur Satu Endpoint Tulis
Operasi tulis (store, update, destroy) adalah tempat bug keamanan paling mahal bersembunyi: mass assignment, validasi yang bocor, otorisasi yang lupa dipasang. Pola yang benar selalu sama untuk ketiganya: Form Request memvalidasi, policy mengotorisasi, model menyimpan, Resource memformat.
Store: Membuat Data
// app/Http/Controllers/Api/PostController.php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Http\Requests\StorePostRequest;
use App\Http\Resources\PostResource;
class PostController extends Controller
{
public function store(StorePostRequest $request)
{
// relasi user()->posts() otomatis mengisi user_id, tidak bisa dipalsukan
$post = $request->user()->posts()->create($request->validated());
return response()->json([
'data' => new PostResource($post->load('user')),
'message' => 'Post berhasil dibuat.',
], 201); // 201 Created, bukan 200
}
}Tiga keputusan penting di sini. Pertama, $request->validated() bukan $request->all(): hanya field lolos validasi yang masuk database, attacker tidak bisa menyelipkan is_admin. Kedua, create lewat relasi $request->user()->posts() sehingga user_id diambil dari token login, bukan dari input user. Kalau user_id diambil dari request body, user bisa membuat post atas nama orang lain. Ketiga, status 201 bukan 200: 201 berarti "resource baru tercipta", dan frontend bisa mengandalkannya.
Update: Mengubah Data
public function update(StorePostRequest $request, Post $post)
{
Gate::authorize('update', $post); // policy: hanya pemilik
$post->update($request->validated());
return new PostResource($post->fresh('user'));
}Gate::authorize() harus datang SEBELUM update(), bukan sesudah. Terlihat sepele, tapi menaruhnya sesudah berarti data sempat berubah sebelum izin dicek, dan kalau ada exception di tengah, kamu mendapat state setengah terotorisasi. Untuk update parsial (PATCH), pakai Form Request terpisah dengan aturan sometimes:
// app/Http/Requests/UpdatePostRequest.php
public function rules(): array
{
return [
'title' => 'sometimes|required|string|max:255',
'body' => 'sometimes|required|string|min:10',
];
}sometimes berarti "validasi hanya kalau field dikirim", jadi client boleh mengirim cuma title tanpa body.
Jebakan klasik di update: aturan unique yang lupa mengecualikan dirinya sendiri.
use Illuminate\Validation\Rule;
'title' => [
'sometimes', 'required', 'string', 'max:255',
Rule::unique('posts', 'title')->ignore($this->route('post')),
],Tanpa ignore(), update tanpa mengubah title selalu gagal validasi karena title-nya sendiri dianggap duplikat.
Destroy: Menghapus Data
public function destroy(Post $post)
{
Gate::authorize('delete', $post);
$post->delete();
return response()->json(null, 204); // 204 No Content: sukses, tanpa body
}Status 204 berarti "berhasil, dan memang tidak ada yang perlu dikembalikan". Jangan kembalikan 200 dengan pesan "berhasil dihapus" kalau kamu sudah sepakat memakai 204, konsistensi lebih penting dari keramahan. Kalau model memakai SoftDeletes, delete() hanya mengisi deleted_at. Itu biasanya yang kamu mau: data bisa dikembalikan, dan audit trail tetap ada.
Peta Status Code
Hafalkan lima ini karena mereka adalah bahasa ibu API-mu:
200OK: operasi baca dan update sukses (atau update, tergantung konvensi tim)201Created: store sukses, sertakan resource baru di body204No Content: destroy sukses, body kosong422Unprocessable: validasi gagal, Laravel mengisi body dengan detail error per field otomatis403Forbidden: policy menolak,404Not Found: binding gagal
Validasi gagal di API otomatis menjadi 422 JSON (bukan redirect) karena Laravel mendeteksi Accept: application/json. Frontend cukup membaca key errors untuk menampilkan pesan per field, pola yang sama seperti @error di Blade.
Jebakan Umum
Pertama, lupa $fillable di model. Post::create($request->validated()) melempar MassAssignmentException kalau field tidak ada di $fillable. Ini fitur keamanan, bukan bug: ia memaksamu mendeklarasikan eksplisit field mana yang boleh diisi massal.
Kedua, tidak mengetes jalur gagal. Kebanyakan developer mengetes POST sukses lalu selesai. Padahal yang membedakan API matang adalah perilaku saat gagal: POST tanpa title harus 422, DELETE milik orang lain harus 403, GET setelah delete harus 404. Tulis test untuk semuanya, bukan cuma jalur bahagia.
Catatan teknis: Endpoint tulis yang baik bisa dibaca seperti kalimat: "validasi request ini, pastikan user boleh, simpan, kembalikan hasilnya." Kalau kamu tidak bisa menjelaskan method controller-mu dalam satu kalimat, ada tanggung jawab yang bocor ke tempat yang salah. Pindahkan sampai kalimatnya sederhana lagi.
Tantangan
Uji CRUD Postman
Dengan Postman: POST 1 post (cek 201), PUT update (cek data berubah), DELETE (cek 204), GET lagi (cek 404). Coba juga POST tanpa title (cek 422 + pesan error JSON).
Tugas
Tugas: API CRUD Produk Lengkap
Bangun API CRUD untuk resource products (nama, harga, stok, kategori) DARI NOL: migration, model + fillable, Form Request (validasi), ApiResource, Api controller (5 method), route apiResource, dan policy (hanya pemilik yang bisa update/delete). Test semua endpoint dengan Postman/curl dan pastikan status code tepat (200/201/204/422/403/404).
Kriteria penilaian:
- Migration dan model benar dengan $fillable yang tepat
- Validasi via Form Request (bukan inline)
- Response memakai ApiResource, status code tepat
- Policy melindungi update/delete milik orang lain
- Semua endpoint ter-test dengan bukti (screenshot/log)
AI tutor akan memeriksa bug, kesalahan syntax, dan memberi saran perbaikan.