Langsung ke konten
KamusNgoding
Menengah Laravel 3 menit baca

Membuat Aplikasi CRUD Lengkap dengan Laravel

#laravel #crud #eloquent #validasi #form-request #blade #intermediate
📚

Baca dulu sebelum ini:

Pendahuluan

Setelah menguasai Eloquent ORM, sekarang saatnya menyatukan semua yang sudah kita pelajari — Routing, Controller, Blade, dan Eloquent — untuk membangun aplikasi CRUD yang benar-benar fungsional.

CRUD (Create, Read, Update, Delete) adalah fondasi hampir semua aplikasi web. Jika kamu bisa membuat CRUD yang bersih di Laravel, kamu sudah siap membangun sistem apapun.

Di artikel ini kita akan menyelesaikan Task Manager secara lengkap dengan fitur:

  • ✅ Listing task dengan pagination
  • ✅ Form buat task baru dengan validasi
  • ✅ Edit task yang sudah ada
  • ✅ Hapus task dengan konfirmasi
  • ✅ Flash message sukses/error
  • ✅ CSRF protection otomatis

Persiapan Awal

Pastikan kamu sudah mengikuti artikel sebelumnya. Model, migration, dan Resource Controller untuk Task sudah dibuat. Jika belum:

# Buat semua yang dibutuhkan sekaligus
php artisan make:model Task -mcr

# Jalankan migration (sesuaikan schema dengan artikel Eloquent sebelumnya)
php artisan migrate

# (Opsional) Isi data dummy
php artisan db:seed --class=TaskSeeder

Verifikasi route yang tersedia:

php artisan route:list --name=tasks

Langkah 1: Update routes/web.php

<?php
// routes/web.php

use App\Http\Controllers\TaskController;
use Illuminate\Support\Facades\Route;

Route::get('/', function () {
    return redirect()->route('tasks.index');
});

Route::resource('tasks', TaskController::class);

Langkah 2: Controller Lengkap

<?php
// app/Http/Controllers/TaskController.php

namespace App\Http\Controllers;

use App\Models\Task;
use App\Http\Requests\StoreTaskRequest;
use App\Http\Requests\UpdateTaskRequest;
use Illuminate\Http\RedirectResponse;
use Illuminate\View\View;

class TaskController extends Controller
{
    // GET /tasks — Tampilkan semua task dengan pagination
    public function index(): View
    {
        $tasks = Task::latest()->paginate(10);
        return view('tasks.index', compact('tasks'));
    }

    // GET /tasks/create — Tampilkan form buat task baru
    public function create(): View
    {
        return view('tasks.create');
    }

    // POST /tasks — Simpan task baru
    public function store(StoreTaskRequest $request): RedirectResponse
    {
        Task::create($request->validated());

        return redirect()
            ->route('tasks.index')
            ->with('success', 'Task berhasil ditambahkan! 🎉');
    }

    // GET /tasks/{task} — Tampilkan detail task
    public function show(Task $task): View
    {
        return view('tasks.show', compact('task'));
    }

    // GET /tasks/{task}/edit — Tampilkan form edit
    public function edit(Task $task): View
    {
        return view('tasks.edit', compact('task'));
    }

    // PUT /tasks/{task} — Update task
    public function update(UpdateTaskRequest $request, Task $task): RedirectResponse
    {
        $task->update($request->validated());

        return redirect()
            ->route('tasks.index')
            ->with('success', 'Task berhasil diperbarui! ✏️');
    }

    // DELETE /tasks/{task} — Hapus task
    public function destroy(Task $task): RedirectResponse
    {
        $task->delete();

        return redirect()
            ->route('tasks.index')
            ->with('success', 'Task berhasil dihapus! 🗑️');
    }
}

Langkah 3: Form Request Validation

Form Request adalah cara terbaik untuk memvalidasi input di Laravel — memisahkan logika validasi dari Controller agar tetap bersih.

php artisan make:request StoreTaskRequest
php artisan make:request UpdateTaskRequest
<?php
// app/Http/Requests/StoreTaskRequest.php

namespace App\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;

class StoreTaskRequest extends FormRequest
{
    // Apakah user boleh melakukan request ini?
    public function authorize(): bool
    {
        return true; // Ubah ke logic auth jika perlu
    }

    // Aturan validasi
    public function rules(): array
    {
        return [
            'title'       => 'required|string|max:255',
            'description' => 'nullable|string|max:1000',
            'priority'    => 'required|in:low,medium,high',
            'due_date'    => 'nullable|date|after_or_equal:today',
        ];
    }

    // Pesan error custom (opsional, tapi ramah pengguna)
    public function messages(): array
    {
        return [
            'title.required'     => 'Judul task wajib diisi.',
            'title.max'          => 'Judul task maksimal 255 karakter.',
            'priority.required'  => 'Prioritas wajib dipilih.',
            'priority.in'        => 'Prioritas tidak valid.',
            'due_date.after_or_equal' => 'Tanggal deadline tidak boleh di masa lalu.',
        ];
    }
}
<?php
// app/Http/Requests/UpdateTaskRequest.php

namespace App\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;

class UpdateTaskRequest extends FormRequest
{
    public function authorize(): bool
    {
        return true;
    }

    public function rules(): array
    {
        return [
            'title'       => 'required|string|max:255',
            'description' => 'nullable|string|max:1000',
            'priority'    => 'required|in:low,medium,high',
            'is_done'     => 'boolean',
            'due_date'    => 'nullable|date',
        ];
    }
}

Langkah 4: Blade Views

Layout Utama

{{-- resources/views/layouts/app.blade.php --}}
<!DOCTYPE html>
<html lang="id">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>@yield('title', 'Task Manager')</title>
    <style>
        * { box-sizing: border-box; margin: 0; padding: 0; }
        body { font-family: sans-serif; background: #f5f5f5; color: #333; }
        .container { max-width: 900px; margin: 0 auto; padding: 20px; }
        nav { background: #4F46E5; color: white; padding: 15px 20px; }
        nav a { color: white; text-decoration: none; font-weight: bold; font-size: 1.2rem; }
        .alert { padding: 12px 16px; border-radius: 6px; margin: 16px 0; }
        .alert-success { background: #d1fae5; color: #065f46; border: 1px solid #6ee7b7; }
        .alert-error   { background: #fee2e2; color: #991b1b; border: 1px solid #fca5a5; }
        .btn { padding: 8px 16px; border-radius: 6px; text-decoration: none; font-size: 0.9rem; cursor: pointer; border: none; }
        .btn-primary  { background: #4F46E5; color: white; }
        .btn-warning  { background: #F59E0B; color: white; }
        .btn-danger   { background: #EF4444; color: white; }
        .btn-secondary { background: #6B7280; color: white; }
    </style>
</head>
<body>
    <nav>
        <a href="{{ route('tasks.index') }}">📋 Task Manager</a>
    </nav>

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

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

        @yield('content')
    </div>
</body>
</html>

Halaman Index

{{-- resources/views/tasks/index.blade.php --}}
@extends('layouts.app')
@section('title', 'Daftar Task')

@section('content')
<div style="display:flex; justify-content:space-between; align-items:center; margin:24px 0 16px;">
    <h1>📋 Daftar Task <span style="color:#6B7280; font-size:1rem;">({{ $tasks->total() }} task)</span></h1>
    <a href="{{ route('tasks.create') }}" class="btn btn-primary">+ Tambah Task</a>
</div>

@forelse ($tasks as $task)
    <div style="background:white; padding:16px; border-radius:8px; margin-bottom:12px; border-left: 4px solid {{ $task->priority === 'high' ? '#EF4444' : ($task->priority === 'medium' ? '#F59E0B' : '#10B981') }};">
        <div style="display:flex; justify-content:space-between; align-items:start;">
            <div>
                <h3 style="{{ $task->is_done ? 'text-decoration:line-through; color:#9CA3AF;' : '' }}">
                    {{ $task->title }}
                </h3>
                @if ($task->description)
                    <p style="color:#6B7280; margin-top:4px;">{{ Str::limit($task->description, 100) }}</p>
                @endif
                <div style="margin-top:8px; font-size:0.8rem; color:#9CA3AF;">
                    <span>Prioritas: <strong>{{ ucfirst($task->priority) }}</strong></span>
                    @if ($task->due_date)
                        &bull; <span>Deadline: {{ $task->due_date->format('d M Y') }}</span>
                    @endif
                    &bull; <span>{{ $task->is_done ? '✅ Selesai' : '⏳ Pending' }}</span>
                </div>
            </div>
            <div style="display:flex; gap:8px; flex-shrink:0;">
                <a href="{{ route('tasks.edit', $task) }}" class="btn btn-warning">Edit</a>
                <form action="{{ route('tasks.destroy', $task) }}" method="POST"
                      onsubmit="return confirm('Yakin ingin menghapus task ini?')">
                    @csrf
                    @method('DELETE')
                    <button type="submit" class="btn btn-danger">Hapus</button>
                </form>
            </div>
        </div>
    </div>
@empty
    <div style="text-align:center; padding:48px; background:white; border-radius:8px;">
        <p style="font-size:1.1rem; color:#6B7280;">Belum ada task. Yuk tambah yang pertama!</p>
        <a href="{{ route('tasks.create') }}" class="btn btn-primary" style="margin-top:16px; display:inline-block;">
            + Tambah Task Pertama
        </a>
    </div>
@endforelse

{{-- Pagination --}}
<div style="margin-top:24px;">
    {{ $tasks->links() }}
</div>
@endsection

Form Tambah Task

{{-- resources/views/tasks/create.blade.php --}}
@extends('layouts.app')
@section('title', 'Tambah Task')

@section('content')
<div style="max-width:600px;">
    <h1 style="margin:24px 0 16px;">➕ Tambah Task Baru</h1>

    <div style="background:white; padding:24px; border-radius:8px;">
        <form action="{{ route('tasks.store') }}" method="POST">
            @csrf

            {{-- Judul --}}
            <div style="margin-bottom:16px;">
                <label style="display:block; font-weight:600; margin-bottom:4px;">
                    Judul Task <span style="color:red;">*</span>
                </label>
                <input type="text" name="title" value="{{ old('title') }}"
                       placeholder="Contoh: Review kode pull request"
                       style="width:100%; padding:8px 12px; border:1px solid #D1D5DB; border-radius:6px; font-size:1rem; {{ $errors->has('title') ? 'border-color:red;' : '' }}">
                @error('title')
                    <span style="color:red; font-size:0.85rem;">{{ $message }}</span>
                @enderror
            </div>

            {{-- Deskripsi --}}
            <div style="margin-bottom:16px;">
                <label style="display:block; font-weight:600; margin-bottom:4px;">Deskripsi</label>
                <textarea name="description" rows="3"
                          placeholder="Tambahkan detail task (opsional)"
                          style="width:100%; padding:8px 12px; border:1px solid #D1D5DB; border-radius:6px; font-size:1rem;">{{ old('description') }}</textarea>
                @error('description')
                    <span style="color:red; font-size:0.85rem;">{{ $message }}</span>
                @enderror
            </div>

            {{-- Prioritas --}}
            <div style="margin-bottom:16px;">
                <label style="display:block; font-weight:600; margin-bottom:4px;">
                    Prioritas <span style="color:red;">*</span>
                </label>
                <select name="priority" style="width:100%; padding:8px 12px; border:1px solid #D1D5DB; border-radius:6px; font-size:1rem;">
                    <option value="low"    {{ old('priority') === 'low'    ? 'selected' : '' }}>🟢 Rendah</option>
                    <option value="medium" {{ old('priority', 'medium') === 'medium' ? 'selected' : '' }}>🟡 Sedang</option>
                    <option value="high"   {{ old('priority') === 'high'   ? 'selected' : '' }}>🔴 Tinggi</option>
                </select>
                @error('priority')
                    <span style="color:red; font-size:0.85rem;">{{ $message }}</span>
                @enderror
            </div>

            {{-- Tanggal Deadline --}}
            <div style="margin-bottom:24px;">
                <label style="display:block; font-weight:600; margin-bottom:4px;">Tanggal Deadline</label>
                <input type="date" name="due_date" value="{{ old('due_date') }}"
                       min="{{ date('Y-m-d') }}"
                       style="width:100%; padding:8px 12px; border:1px solid #D1D5DB; border-radius:6px; font-size:1rem;">
                @error('due_date')
                    <span style="color:red; font-size:0.85rem;">{{ $message }}</span>
                @enderror
            </div>

            <div style="display:flex; gap:12px;">
                <button type="submit" class="btn btn-primary">💾 Simpan Task</button>
                <a href="{{ route('tasks.index') }}" class="btn btn-secondary">Batal</a>
            </div>
        </form>
    </div>
</div>
@endsection

Form Edit Task

{{-- resources/views/tasks/edit.blade.php --}}
@extends('layouts.app')
@section('title', 'Edit Task')

@section('content')
<div style="max-width:600px;">
    <h1 style="margin:24px 0 16px;">✏️ Edit Task</h1>

    <div style="background:white; padding:24px; border-radius:8px;">
        <form action="{{ route('tasks.update', $task) }}" method="POST">
            @csrf
            @method('PUT')

            {{-- Judul --}}
            <div style="margin-bottom:16px;">
                <label style="display:block; font-weight:600; margin-bottom:4px;">Judul Task *</label>
                <input type="text" name="title" value="{{ old('title', $task->title) }}"
                       style="width:100%; padding:8px 12px; border:1px solid #D1D5DB; border-radius:6px; font-size:1rem;">
                @error('title')
                    <span style="color:red; font-size:0.85rem;">{{ $message }}</span>
                @enderror
            </div>

            {{-- Deskripsi --}}
            <div style="margin-bottom:16px;">
                <label style="display:block; font-weight:600; margin-bottom:4px;">Deskripsi</label>
                <textarea name="description" rows="3"
                          style="width:100%; padding:8px 12px; border:1px solid #D1D5DB; border-radius:6px; font-size:1rem;">{{ old('description', $task->description) }}</textarea>
            </div>

            {{-- Prioritas --}}
            <div style="margin-bottom:16px;">
                <label style="display:block; font-weight:600; margin-bottom:4px;">Prioritas</label>
                <select name="priority" style="width:100%; padding:8px 12px; border:1px solid #D1D5DB; border-radius:6px; font-size:1rem;">
                    @foreach (['low' => '🟢 Rendah', 'medium' => '🟡 Sedang', 'high' => '🔴 Tinggi'] as $val => $label)
                        <option value="{{ $val }}" {{ old('priority', $task->priority) === $val ? 'selected' : '' }}>
                            {{ $label }}
                        </option>
                    @endforeach
                </select>
            </div>

            {{-- Status Selesai --}}
            <div style="margin-bottom:16px;">
                <label style="display:flex; align-items:center; gap:8px; cursor:pointer;">
                    <input type="hidden" name="is_done" value="0">
                    <input type="checkbox" name="is_done" value="1" {{ old('is_done', $task->is_done) ? 'checked' : '' }}>
                    <span>Tandai sebagai selesai</span>
                </label>
            </div>

            {{-- Tanggal Deadline --}}
            <div style="margin-bottom:24px;">
                <label style="display:block; font-weight:600; margin-bottom:4px;">Tanggal Deadline</label>
                <input type="date" name="due_date"
                       value="{{ old('due_date', $task->due_date?->format('Y-m-d')) }}"
                       style="width:100%; padding:8px 12px; border:1px solid #D1D5DB; border-radius:6px; font-size:1rem;">
            </div>

            <div style="display:flex; gap:12px;">
                <button type="submit" class="btn btn-primary">💾 Update Task</button>
                <a href="{{ route('tasks.index') }}" class="btn btn-secondary">Batal</a>
            </div>
        </form>
    </div>
</div>
@endsection

Langkah 5: Update Model Task

Pastikan Model Task sudah dikonfigurasi dengan benar:

<?php
// app/Models/Task.php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class Task extends Model
{
    protected $fillable = [
        'title', 'description', 'priority', 'is_done', 'due_date'
    ];

    protected $casts = [
        'is_done'  => 'boolean',
        'due_date' => 'date',
    ];
}

Menjalankan Aplikasi

php artisan serve

Buka http://localhost:8000 dan coba semua fitur:

  1. Tambah beberapa task
  2. Edit task yang sudah ada
  3. Tandai task sebagai selesai
  4. Hapus task

Troubleshooting: Error yang Sering Muncul

CSRF token mismatch (419 Error)

Penyebab: Semua form POST/PUT/DELETE harus menyertakan token CSRF, tapi @csrf tidak ada di form.

Solusi:

<form method="POST" action="{{ route('tasks.store') }}">
    @csrf  {{-- WAJIB ada di setiap form POST/PUT/DELETE --}}
    ...
</form>

MethodNotAllowedHttpException untuk PUT/DELETE

Penyebab: Browser hanya mendukung GET dan POST. Untuk PUT, PATCH, DELETE harus menggunakan method spoofing.

Solusi:

<form action="{{ route('tasks.update', $task) }}" method="POST">
    @csrf
    @method('PUT')   {{-- Wajib untuk form edit --}}
    ...
</form>

<form action="{{ route('tasks.destroy', $task) }}" method="POST">
    @csrf
    @method('DELETE')  {{-- Wajib untuk form hapus --}}
    ...
</form>

Pesan Validasi Tidak Muncul

Penyebab: Lupa menampilkan error di Blade, atau menggunakan validate() di Controller tapi tidak ada @error di view.

Solusi:

{{-- Tampilkan semua error sekaligus --}}
@if ($errors->any())
    <div class="alert alert-error">
        <ul>
            @foreach ($errors->all() as $error)
                <li>{{ $error }}</li>
            @endforeach
        </ul>
    </div>
@endif

{{-- Atau tampilkan per field --}}
@error('title')
    <span style="color:red;">{{ $message }}</span>
@enderror

Pertanyaan yang Sering Diajukan

Apa bedanya validate() di Controller vs Form Request?

validate() langsung di Controller cepat untuk kasus sederhana. Form Request lebih baik untuk aplikasi nyata karena: logika validasi terpisah dari Controller, bisa reuse, lebih mudah di-test, dan mendukung custom authorization. Gunakan Form Request untuk semua fitur production.

Bagaimana cara menambahkan pagination di Laravel?

// Controller — ganti get() dengan paginate()
$tasks = Task::latest()->paginate(15); // 15 item per halaman

// Blade — tambahkan di bawah loop
{{ $tasks->links() }} // Tampilkan link halaman

Untuk custom styling pagination, jalankan php artisan vendor:publish --tag=laravel-pagination.

Bagaimana cara menggunakan old() di form?

Fungsi old('field_name') mengembalikan nilai input sebelumnya — sangat berguna agar form tidak kosong setelah validasi gagal. Selalu tambahkan old() di semua field form:

<input name="title" value="{{ old('title', $task->title ?? '') }}">
{{-- old('title') → nilai dari request terakhir
     $task->title → nilai default saat edit
     '' → nilai default kosong saat create --}}

Kesimpulan

Selamat! Aplikasi CRUD Task Manager kamu sekarang sudah berfungsi penuh. Kita sudah membangun:

  • Controller lengkap dengan 7 method CRUD
  • Form Request Validation yang bersih dan terorganisir
  • Blade views: index, create, edit dengan pesan validasi
  • Flash message untuk feedback sukses/error
  • CSRF protection dan method spoofing

Di artikel berikutnya, kita akan membahas topik yang sangat sering menyebabkan masalah bagi developer Indonesia: Cara Mengatasi Error CORS di Laravel — terutama saat menghubungkan Laravel backend dengan React/Vue frontend.

Kamu sudah berhasil membangun aplikasi web yang nyata! Ini adalah pencapaian yang luar biasa 🏆

Artikel Terkait