Laravel CRUD Tutorial
LaravelCRUD (Create, Read, Update, Delete) is the foundation of almost every web application. In this tutorial you'll build a complete product management system from scratch in Laravel.
What We're Building
Overview
Product Management App
├── GET /products → List all products
├── GET /products/create → Show create form
├── POST /products → Save new product
├── GET /products/{id} → Show single product
├── GET /products/{id}/edit → Show edit form
├── PUT /products/{id} → Update product
└── DELETE /products/{id} → Delete productStep 1: Create Migration
Terminal
php artisan make:model Product -mcr
# -m = migration, -c = controller, -r = resource controllerdatabase/migrations/create_products_table.php
<?php
public function up(): void
{
Schema::create('products', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->text('description')->nullable();
$table->decimal('price', 10, 2);
$table->integer('stock')->default(0);
$table->string('image')->nullable();
$table->boolean('active')->default(true);
$table->timestamps();
});
}
// Run migration
// php artisan migrateStep 2: Product Model
app/Models/Product.php
<?php
class Product extends Model
{
protected $fillable = [
'name', 'description', 'price', 'stock', 'image', 'active'
];
protected $casts = [
'price' => 'decimal:2',
'active' => 'boolean',
];
// Formatted price accessor
public function getFormattedPriceAttribute(): string {
return '¥' . number_format($this->price, 0);
}
// Scope for active products
public function scopeActive($query) {
return $query->where('active', true);
}
}
?>Step 3: Resource Controller
app/Http/Controllers/ProductController.php
<?php
namespace App\Http\Controllers;
use App\Models\Product;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Storage;
class ProductController extends Controller
{
// GET /products
public function index(Request $request)
{
$products = Product::query()
->when($request->search, fn($q) =>
$q->where('name', 'like', "%{$request->search}%"))
->when($request->active, fn($q) =>
$q->where('active', $request->active === 'yes'))
->latest()
->paginate(10)
->withQueryString();
return view('products.index', compact('products'));
}
// GET /products/create
public function create()
{
return view('products.create');
}
// POST /products
public function store(Request $request)
{
$validated = $request->validate([
'name' => 'required|string|max:255',
'description' => 'nullable|string',
'price' => 'required|numeric|min:0',
'stock' => 'required|integer|min:0',
'image' => 'nullable|image|max:2048',
'active' => 'boolean',
]);
if ($request->hasFile('image')) {
$validated['image'] = $request->file('image')
->store('products', 'public');
}
Product::create($validated);
return redirect()
->route('products.index')
->with('success', 'Product created successfully!');
}
// GET /products/{product}
public function show(Product $product)
{
return view('products.show', compact('product'));
}
// GET /products/{product}/edit
public function edit(Product $product)
{
return view('products.edit', compact('product'));
}
// PUT /products/{product}
public function update(Request $request, Product $product)
{
$validated = $request->validate([
'name' => 'required|string|max:255',
'description' => 'nullable|string',
'price' => 'required|numeric|min:0',
'stock' => 'required|integer|min:0',
'image' => 'nullable|image|max:2048',
'active' => 'boolean',
]);
if ($request->hasFile('image')) {
if ($product->image) {
Storage::disk('public')->delete($product->image);
}
$validated['image'] = $request->file('image')
->store('products', 'public');
}
$product->update($validated);
return redirect()
->route('products.index')
->with('success', 'Product updated successfully!');
}
// DELETE /products/{product}
public function destroy(Product $product)
{
if ($product->image) {
Storage::disk('public')->delete($product->image);
}
$product->delete();
return redirect()
->route('products.index')
->with('success', 'Product deleted!');
}
}
?>Step 4: Routes
routes/web.php
<?php
use App\Http\Controllers\ProductController;
Route::resource('products', ProductController::class);
// This one line creates ALL 7 routes:
// GET /products products.index
// GET /products/create products.create
// POST /products products.store
// GET /products/{id} products.show
// GET /products/{id}/edit products.edit
// PUT /products/{id} products.update
// DELETE /products/{id} products.destroy
?>Step 5: Views
resources/views/products/index.blade.php
@extends('layouts.app')
@section('title', 'Products')
@section('content')
<div class="max-w-6xl mx-auto p-6">
<!-- Header -->
<div class="flex justify-between items-center mb-6">
<h1 class="text-2xl font-bold">Products ({{ $products->total() }})</h1>
<a href="{{ route('products.create') }}"
class="bg-blue-600 text-white px-4 py-2 rounded hover:bg-blue-700">
+ Add Product
</a>
</div>
<!-- Search bar -->
<form method="GET" class="mb-6 flex gap-2">
<input type="text" name="search" value="{{ request('search') }}"
placeholder="Search products..."
class="flex-1 border rounded px-3 py-2" />
<button type="submit" class="bg-gray-600 text-white px-4 py-2 rounded">Search</button>
</form>
@if(session('success'))
<div class="bg-green-100 text-green-800 p-3 rounded mb-4">{{ session('success') }}</div>
@endif
<!-- Products Table -->
<div class="bg-white rounded shadow overflow-hidden">
<table class="w-full text-sm">
<thead class="bg-gray-50">
<tr>
<th class="p-4 text-left">Image</th>
<th class="p-4 text-left">Name</th>
<th class="p-4 text-left">Price</th>
<th class="p-4 text-left">Stock</th>
<th class="p-4 text-left">Status</th>
<th class="p-4 text-left">Actions</th>
</tr>
</thead>
<tbody class="divide-y">
@forelse($products as $product)
<tr class="hover:bg-gray-50">
<td class="p-4">
@if($product->image)
<img src="{{ Storage::url($product->image) }}"
class="w-12 h-12 object-cover rounded" />
@else
<div class="w-12 h-12 bg-gray-200 rounded flex items-center justify-center text-gray-400">?</div>
@endif
</td>
<td class="p-4 font-medium">{{ $product->name }}</td>
<td class="p-4">{{ $product->formatted_price }}</td>
<td class="p-4">
<span class="{{ $product->stock < 5 ? 'text-red-600 font-bold' : '' }}">
{{ $product->stock }}
</span>
</td>
<td class="p-4">
@if($product->active)
<span class="bg-green-100 text-green-700 px-2 py-1 rounded text-xs">Active</span>
@else
<span class="bg-red-100 text-red-700 px-2 py-1 rounded text-xs">Inactive</span>
@endif
</td>
<td class="p-4">
<div class="flex gap-3">
<a href="{{ route('products.edit', $product) }}" class="text-blue-600 hover:underline">Edit</a>
<form method="POST" action="{{ route('products.destroy', $product) }}"
onsubmit="return confirm('Delete this product?')">
@csrf @method('DELETE')
<button class="text-red-600 hover:underline">Delete</button>
</form>
</div>
</td>
</tr>
@empty
<tr><td colspan="6" class="p-8 text-center text-gray-400">No products found.</td></tr>
@endforelse
</tbody>
</table>
</div>
<div class="mt-4">{{ $products->links() }}</div>
</div>
@endsection💡
Route::resource() is magic! One line creates 7 routes with proper HTTP methods (GET, POST, PUT, DELETE). Always use it for standard CRUD — never manually write all 7 routes.
💡
Always use Route Model Binding!
public function show(Product $product) — Laravel automatically finds the product by ID and throws a 404 if not found. No need to write Product::findOrFail($id) manually.Test your knowledge!
Take a quiz to reinforce what you learned.