Cara Membuat CRUD Laravel 11 dari Nol untuk Pemula

Anda sudah memasang Laravel, halaman welcome sudah muncul, lalu bingung harus mulai dari mana. Tutorial di internet sering melompat-lompat: sebagian masih memakai gaya Laravel 8, s...

Cara Membuat CRUD Laravel 11 dari Nol untuk Pemula

Anda sudah memasang Laravel, halaman welcome sudah muncul, lalu bingung harus mulai dari mana. Tutorial di internet sering melompat-lompat: sebagian masih memakai gaya Laravel 8, sebagian langsung memakai paket tambahan yang belum Anda butuhkan. Hasilnya, CRUD jalan setengah dan muncul error yang tidak Anda pahami.

Panduan ini membangun CRUD produk lengkap di Laravel 11 secara berurutan: tabel, model, controller, validasi, tampilan, pencarian, dan hapus data. Setiap langkah disertai alasan, supaya Anda bisa mengulang pola yang sama untuk pelanggan, kategori, atau transaksi tanpa menyalin buta.

Persiapan

Pastikan PHP 8.2+, Composer, dan MySQL atau MariaDB sudah terpasang. Jika belum, ikuti panduan setup environment PHP, Composer, dan MySQL. Buat project baru lalu atur database di .env:

composer create-project laravel/laravel toko
cd toko
# ubah DB_CONNECTION=mysql, DB_DATABASE=toko, DB_USERNAME, DB_PASSWORD di .env
php artisan serve

Laravel 11 memakai SQLite sebagai default. Jika Anda ingin MySQL, ubah DB_CONNECTION dan buat database kosongnya terlebih dahulu.

Gambaran Alur CRUD

AksiMethod HTTPURLMethod controller
Daftar produkGET/productsindex
Form tambahGET/products/createcreate
Simpan baruPOST/productsstore
DetailGET/products/{product}show
Form ubahGET/products/{product}/editedit
Simpan perubahanPUT/PATCH/products/{product}update
HapusDELETE/products/{product}destroy

Tabel ini adalah konvensi resource Laravel. Mengikutinya membuat satu baris route cukup untuk ketujuh aksi.

Langkah 1: Migration dan Model

php artisan make:model Product -mf

Flag -m membuat migration dan -f membuat factory untuk data uji. Isi migration-nya:

public function up(): void
{
    Schema::create('products', function (Blueprint $table) {
        $table->id();
        $table->string('name');
        $table->unsignedInteger('price');
        $table->unsignedInteger('stock')->default(0);
        $table->text('description')->nullable();
        $table->timestamps();
    });
}
php artisan migrate

Kenapa migration, bukan membuat tabel lewat phpMyAdmin? Karena struktur tabel ikut tersimpan di Git. Rekan tim atau server produksi cukup menjalankan migrate untuk mendapatkan struktur yang sama persis. Harga disimpan sebagai bilangan bulat karena Rupiah tidak memakai sen; untuk mata uang dengan desimal, gunakan decimal(12, 2).

Lalu daftarkan kolom yang boleh diisi massal di model:

class Product extends Model
{
    use HasFactory;

    protected $fillable = ['name', 'price', 'stock', 'description'];
}

$fillable melindungi dari mass assignment: tanpa daftar ini, penyerang bisa menyisipkan field tambahan di form dan mengubah kolom yang tidak seharusnya.

Langkah 2: Controller dan Route

php artisan make:controller ProductController --resource --model=Product
php artisan make:request StoreProductRequest
// routes/web.php
use App\Http\Controllers\ProductController;

Route::resource('products', ProductController::class);

Jalankan php artisan route:list --path=products untuk melihat ketujuh route yang terbentuk.

Langkah 3: Validasi dengan Form Request

// app/Http/Requests/StoreProductRequest.php
public function authorize(): bool
{
    return true; // ganti dengan pengecekan hak akses bila perlu
}

public function rules(): array
{
    return [
        'name'        => 'required|string|max:255',
        'price'       => 'required|integer|min:0',
        'stock'       => 'required|integer|min:0',
        'description' => 'nullable|string|max:2000',
    ];
}

Method authorize() bawaan mengembalikan false. Jika lupa diubah, setiap submit akan berakhir dengan error 403. Kelebihan Form Request: aturan validasi dipakai ulang untuk store dan update, dan controller tetap pendek.

Langkah 4: Isi Controller

public function index(Request $request)
{
    $products = Product::query()
        ->when($request->q, fn ($query, $q) => $query->where('name', 'like', "%{$q}%"))
        ->latest()
        ->paginate(10)
        ->withQueryString();

    return view('products.index', compact('products'));
}

public function create()
{
    return view('products.create');
}

public function store(StoreProductRequest $request)
{
    Product::create($request->validated());

    return redirect()->route('products.index')->with('success', 'Produk ditambahkan.');
}

public function edit(Product $product)
{
    return view('products.edit', compact('product'));
}

public function update(StoreProductRequest $request, Product $product)
{
    $product->update($request->validated());

    return redirect()->route('products.index')->with('success', 'Produk diperbarui.');
}

public function destroy(Product $product)
{
    $product->delete();

    return back()->with('success', 'Produk dihapus.');
}

Parameter Product $product adalah route model binding: Laravel mencari produk berdasarkan ID di URL dan otomatis mengembalikan 404 jika tidak ditemukan. $request->validated() hanya berisi field yang lolos aturan, sehingga input liar tidak ikut tersimpan. withQueryString() menjaga kata kunci pencarian tetap terbawa saat pindah halaman.

Langkah 5: Tampilan Blade

Form tambah dan ubah hampir identik, jadi cukup satu partial resources/views/products/_form.blade.php:

@csrf
<label>Nama</label>
<input name="name" value="{{ old('name', $product->name ?? '') }}">
@error('name') <small>{{ $message }}</small> @enderror

<label>Harga</label>
<input type="number" name="price" value="{{ old('price', $product->price ?? '') }}">
@error('price') <small>{{ $message }}</small> @enderror

<label>Stok</label>
<input type="number" name="stock" value="{{ old('stock', $product->stock ?? 0) }}">

<button type="submit">Simpan</button>

Di edit.blade.php, bungkus partial tersebut dengan form yang memalsukan method PUT, karena browser hanya mengenal GET dan POST:

<form method="POST" action="{{ route('products.update', $product) }}">
    @method('PUT')
    @include('products._form')
</form>

Untuk tombol hapus di halaman daftar:

<form method="POST" action="{{ route('products.destroy', $product) }}"
      onsubmit="return confirm('Hapus produk ini?')">
    @csrf
    @method('DELETE')
    <button type="submit">Hapus</button>
</form>

Fungsi old() mengembalikan isian sebelumnya saat validasi gagal, sehingga pengguna tidak perlu mengetik ulang. Tampilkan {{ $products->links() }} di bawah tabel untuk navigasi halaman.

Langkah 6: Halaman Daftar dan Pesan Sukses

Halaman index.blade.php menampilkan pesan dari session, form pencarian, dan tabel produk:

@if (session('success'))
    <div class="alert">{{ session('success') }}</div>
@endif

<form method="GET" action="{{ route('products.index') }}">
    <input name="q" value="{{ request('q') }}" placeholder="Cari produk">
</form>

<table>
@forelse ($products as $product)
    <tr>
        <td>{{ $product->name }}</td>
        <td>Rp{{ number_format($product->price, 0, ',', '.') }}</td>
        <td>{{ $product->stock }}</td>
        <td><a href="{{ route('products.edit', $product) }}">Ubah</a></td>
    </tr>
@empty
    <tr><td colspan="4">Belum ada produk.</td></tr>
@endforelse
</table>

{{ $products->links() }}

@forelse menangani kondisi tabel kosong sekaligus, sehingga pengguna baru tidak melihat halaman putih tanpa penjelasan. Kurung kurawal ganda {{ }} otomatis meng-escape HTML, jadi nama produk berisi tag script tidak akan dieksekusi.

Langkah 7: Isi Data Contoh dengan Factory

Menguji pagination dengan dua produk tidak akan memperlihatkan masalah. Isi factory yang dibuat di langkah 1:

// database/factories/ProductFactory.php
public function definition(): array
{
    return [
        'name'  => fake()->words(2, true),
        'price' => fake()->numberBetween(5000, 250000),
        'stock' => fake()->numberBetween(0, 100),
    ];
}

Lalu jalankan php artisan tinker dan ketik Product::factory()->count(50)->create();. Sekarang Anda bisa mencoba pencarian dan perpindahan halaman dengan data yang lebih realistis.

Error Umum dan Cara Mengatasinya

Pesan errorPenyebabPerbaikan
419 Page ExpiredForm tanpa @csrf atau session kedaluwarsaTambahkan @csrf di setiap form POST
MassAssignmentExceptionKolom belum ada di $fillableTambahkan nama kolom ke $fillable
403 This action is unauthorizedauthorize() di Form Request masih falseKembalikan true atau logika hak akses
405 Method Not AllowedLupa @method('PUT') atau @method('DELETE')Tambahkan direktif method di form
Target class does not existImport controller lupa di routes/web.phpTambahkan baris use yang benar
SQLSTATE[HY000] [2002]Database tidak bisa dihubungiCek .env lalu jalankan php artisan config:clear

Cara membaca pesan error

Saat halaman merah Laravel muncul, jangan langsung menyalin seluruh teks ke mesin pencari. Baca baris pertama untuk jenis error, lalu cari frame pertama di stack trace yang menunjuk file milik Anda di folder app/ atau resources/views/. Biasanya di situlah letak salah ketik, import yang terlupa, atau nama variabel yang tidak dikirim dari controller. File storage/logs/laravel.log juga menyimpan error yang sama bila halaman sudah ditutup.

Checklist CRUD yang Layak Dipakai

  • Semua input divalidasi di Form Request, termasuk saat update.
  • Setiap form punya @csrf, form ubah dan hapus punya @method.
  • Pesan sukses tampil setelah simpan, ubah, dan hapus.
  • Daftar data berpaginasi dan bisa dicari.
  • Penghapusan meminta konfirmasi.
  • Route CRUD berada di dalam middleware auth sebelum dipasang di server.

Langkah Berikutnya

Setelah CRUD tunggal lancar, tambahkan relasi (misalnya produk milik kategori) dan rancang strukturnya lebih dulu dengan panduan merancang ERD sederhana. Kemudian lindungi route dengan autentikasi dan periksa poin-poin di checklist keamanan dasar Laravel sebelum aplikasi dipakai orang lain. Pola tujuh aksi di atas akan Anda temui berulang kali, dari aplikasi kasir hingga sistem inventaris.

Ingin langsung mempraktikkannya?

Buat akun gratis untuk mengunduh contoh aplikasi Laravel, menyimpan tutorial favorit, dan mencoba kursus. Gratis, bisa daftar dengan akun Google.

Daftar Gratis Lihat katalog aplikasi
Yudhi
Ditulis oleh
Yudhi
Founder & Lead Developer, GudangCode

Yudhi adalah founder GudangCode dan developer Laravel dengan pengalaman membangun puluhan sistem informasi bisnis siap pakai — mulai dari POS, HRIS, hingga aplikasi manajemen. Ia menulis panduan dan artikel di GudangCode untuk membantu developer Indonesia menjalankan, memahami, dan men-deploy source code Laravel dengan benar.

Laravel PHP MySQL Sistem Informasi Bisnis
Lihat semua artikel Yudhi
Laravel PHP CRUD Tutorial
Bacaan Paling Dicari
📚 Free Learning Hub

Learn Coding for Free at DhieCoderWeb

Explore Laravel, PHP, JavaScript tutorials, source code, web development guides, and practical programming tips.

DhieCoderWeb
100+
Tutorials
Free
Learning
SEO
Tips
Visit Dhiecoderweb.com →