Introduction
CRUD (Create, Read, Update, Delete) operations form the foundation of most web applications. Laravel makes implementing these operations elegant and efficient through its powerful Eloquent ORM, Resource Controllers, Blade templating, and built-in validation.
In this comprehensive tutorial, we'll build a Product Management System — a practical example where you can create, view, edit, and delete products with features like:
- Form validation
- Image upload and storage
- Pagination
- Success/error messages
- Bootstrap 5 styling for a clean UI
This guide works with Laravel 11 or later (including Laravel 12/13 concepts as of 2026). We'll use the latest best practices.
Prerequisites
- PHP 8.2+, Composer, MySQL/MariaDB (or another supported database), Basic knowledge of PHP and MVC pattern
Step 1: Install Laravel
Open your terminal and run:
composer create-project laravel/laravel laravel-crud-app
cd laravel-crud-appStart the development server later with `php artisan serve`.
Step 2: Configure the Database
Update your `.env` file with database credentials:
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=laravel_crud
DB_USERNAME=root
DB_PASSWORD=Create the database in MySQL and run:
php artisan migrateStep 3: Create Model, Migration, and Resource Controller
We'll create a Product model with migration and a full Resource Controller in one go:
php artisan make:model Product -mcrThe `-mcr` flag generates:
- Model (app/Models/Product.php)
- Migration (database/migrations/xxxx_xx_xx_create_products_table.php)
- Resource Controller (app/Http/Controllers/ProductController.php)
Step 4: Define the Products Table (Migration)
Open the migration file and update the up() method:
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->string('image')->nullable();
$table->timestamps();
});
}Run the migration:
php artisan migrateStep 5: Update the Product Model
In app/Models/Product.php, add mass assignment protection:
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class Product extends Model
{
use HasFactory;
protected $fillable = [
'name',
'description',
'price',
'image',
];
}Step 6: Set Up Resource Routes
Open routes/web.php and register the resource route:
use Illuminate\Support\Facades\Route;
use App\Http\Controllers\ProductController;
Route::resource('products', ProductController::class);This single line creates all 7 RESTful routes:
- `GET /products` → index
- `GET /products/create` → create
- `POST /products` → store
- `GET /products/{product}` → show
- `GET /products/{product}/edit` → edit
- `PUT/PATCH /products/{product}` → update
- `DELETE /products/{product}` → destroy
Step 7: Create Form Request for Validation (Optional but Recommended)
Generate validation requests:
php artisan make:request StoreProductRequest
php artisan make:request UpdateProductRequestIn app/Http/Requests/StoreProductRequest.php:
public function rules(): array
{
return [
'name' => 'required|string|max:255',
'description' => 'nullable|string',
'price' => 'required|numeric|min:0',
'image' => 'nullable|image|mimes:jpeg,png,jpg,gif|max:2048',
];
}Do the same for `UpdateProductRequest` (make `image` optional for updates).
Step 8: Implement the Resource Controller
Open app/Http/Controllers/ProductController.php and fill in the methods:
namespace App\Http\Controllers;
use App\Models\Product;
use App\Http\Requests\StoreProductRequest;
use App\Http\Requests\UpdateProductRequest;
use Illuminate\Support\Facades\Storage;
use Illuminate\Http\Request;
class ProductController extends Controller
{
public function index()
{
$products = Product::latest()->paginate(10);
return view('products.index', compact('products'));
}
public function create()
{
return view('products.create');
}
public function store(StoreProductRequest $request)
{
$data = $request->validated();
if ($request->hasFile('image')) {
$data['image'] = $request->file('image')->store('products', 'public');
}
Product::create($data);
return redirect()->route('products.index')
->with('success', 'Product created successfully!');
}
public function show(Product $product)
{
return view('products.show', compact('product'));
}
public function edit(Product $product)
{
return view('products.edit', compact('product'));
}
public function update(UpdateProductRequest $request, Product $product)
{
$data = $request->validated();
if ($request->hasFile('image')) {
if ($product->image) {
Storage::disk('public')->delete($product->image);
}
$data['image'] = $request->file('image')->store('products', 'public');
}
$product->update($data);
return redirect()->route('products.index')
->with('success', 'Product updated successfully!');
}
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 successfully!');
}
}Note: We're using Route Model Binding (`Product $product`) for clean code.
Step 9: Create Blade Views
Create the `resources/views/products/` directory and add these files.
index.blade.php (List with Pagination)
@extends('layouts.app')
@section('content')
<div class="container mt-4">
<h1>Products</h1>
<a href="{{ route('products.create') }}" class="btn btn-primary mb-3">Create New Product</a>
@if (session('success'))
<div class="alert alert-success">{{ session('success') }}</div>
@endif
<table class="table table-striped">
<thead>
<tr>
<th>ID</th>
<th>Image</th>
<th>Name</th>
<th>Price</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
@foreach($products as $product)
<tr>
<td>{{ $product->id }}</td>
<td>
@if($product->image)
<img src="{{ Storage::url($product->image) }}" width="80" alt="{{ $product->name }}">
@endif
</td>
<td>{{ $product->name }}</td>
<td>${{ number_format($product->price, 2) }}</td>
<td>
<a href="{{ route('products.show', $product) }}" class="btn btn-info btn-sm">View</a>
<a href="{{ route('products.edit', $product) }}" class="btn btn-warning btn-sm">Edit</a>
<form action="{{ route('products.destroy', $product) }}" method="POST" class="d-inline">
@csrf
@method('DELETE')
<button type="submit" class="btn btn-danger btn-sm" onclick="return confirm('Are you sure?')">Delete</button>
</form>
</td>
</tr>
@endforeach
</tbody>
</table>
{{ $products->links() }}
</div>
@endsectioncreate.blade.php and edit.blade.php
Use similar forms. Here's a basic structure for create.blade.php:
@extends('layouts.app')
@section('content')
<div class="container mt-4">
<h1>Create Product</h1>
<form action="{{ route('products.store') }}" method="POST" enctype="multipart/form-data">
@csrf
<div class="mb-3">
<label>Name</label>
<input type="text" name="name" class="form-control" value="{{ old('name') }}">
@error('name') <span class="text-danger">{{ $message }}</span> @enderror
</div>
<!-- Add fields for description, price, image -->
<div class="mb-3">
<label>Image</label>
<input type="file" name="image" class="form-control">
</div>
<button type="submit" class="btn btn-success">Save</button>
</form>
</div>
@endsectionFor edit.blade.php, use @method('PUT') and populate values with $product->name, etc.
`show.blade.php`
Simple detail view displaying all product info and image.
Step 10: Layout and Bootstrap
Publish or create a basic `resources/views/layouts/app.blade.php` with Bootstrap 5 CDN for quick styling:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Laravel CRUD</title>
<link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
@yield('content')
</body>
</html>For better styling, install Laravel Breeze or use Vite with Bootstrap.
Step 11: Storage Link for Images
Run this command to link `public/storage` to `storage/app/public`:
php artisan storage:linkStep 12: Test Your Application
Start the server:
php artisan serveVisit `http://127.0.0.1:8000/products` and test all CRUD operations.
Best Practices & Enhancements
- Use Form Requests for validation to keep controllers clean.
- Eloquent Relationships — Extend this with categories, users, etc.
- Soft Deletes — Add `SoftDeletes` trait for safe deletion.
- API Version — For APIs, use `php artisan make:controller ProductController --api` and API Resources.
- Authorization — Add Gates/Policies for production apps.
- Search & Filters — Add query parameters in `index()` method.
- Testing — Write feature tests for each CRUD action.
Conclusion
You've now built a fully functional Laravel CRUD application! This pattern scales well and serves as a solid foundation for larger projects like blogs, e-commerce, or admin panels.
Laravel's Resource Controllers and Eloquent drastically reduce boilerplate, letting you focus on business logic.
Next Steps:
- Add authentication with Laravel Breeze or Jetstream.
- Implement Livewire or Inertia.js for modern SPA-like feel.
- Deploy to platforms like Forge, Vapor, or shared hosting.
Source Code: You can find similar complete examples on GitHub by searching "Laravel 13 CRUD".
Happy coding! If you run into issues or want to extend this (e.g., with relationships or API), drop your questions in the comments.
This tutorial is based on Laravel's official patterns and community best practices as of 2026.