Table of Contents
A Laravel CRUD application is the foundation of almost every web project – blog systems, admin dashboards, product catalogs, user management. Once you understand how Create, Read, Update, and Delete work in Laravel, every real-world application becomes a variation of the same pattern.
This guide builds a complete product management system from a fresh Laravel install to working CRUD with validation, flash messages, and proper Blade views. Every step includes the actual code you need to run – not just snippets.
What You Will Build
A products table with full CRUD functionality:
- List all products with pagination
- Create a new product with form validation
- View a single product
- Edit and update an existing product
- Delete a product with confirmation
Prerequisites
- PHP 8.1 or higher
- Composer installed
- MySQL database
- Laravel 10 or higher
The complete source code for this project is available on GitHub.
Watch Laravel CRUD in Action (Video Demo)
This Laravel CRUD application step by step guide also includes a working video demo so you can understand the complete flow visually.
What This Demo Shows
- Creating a new record using a form
- Displaying records in a list
- Updating existing data
- Deleting records safely
Step 1: Create a New Laravel Project
composer create-project laravel/laravel crud-app
cd crud-app
php artisan serve
Output:
INFO Server running on [http://127.0.0.1:8000].
Open http://127.0.0.1:8000 in your browser. You should see the Laravel welcome page.
Step 2: Configure the Database
Copy the example environment file and configure your database credentials:
cp .env.example .env
php artisan key:generate
Open .env and update the database section:
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=crud_app
DB_USERNAME=root
DB_PASSWORD=your_password
Create the database in MySQL:
CREATE DATABASE crud_app CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
Step 3: Create Model and Migration
The -m flag creates the migration alongside the model:
php artisan make:model Product -m
Output:
INFO Model [app/Models/Product.php] created successfully.
INFO Migration [database/migrations/2026_05_01_000000_create_products_table.php] created successfully.
Open the migration file in database/migrations/ and define the columns:
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
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->unsignedInteger('quantity')->default(0);
$table->timestamps();
});
}
public function down(): void
{
Schema::dropIfExists('products');
}
};
Run the migration:
php artisan migrate
Output:
INFO Running migrations.
2026_05_01_000000_create_products_table ................. 12ms DONE
Open app/Models/Product.php and add the fillable fields – without this, mass assignment (used by create() and update()) won’t work:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Product extends Model
{
protected $fillable = [
'name',
'description',
'price',
'quantity',
];
}
Step 4: Create the Resource Controller
php artisan make:controller ProductController --resource --model=Product
The --resource flag generates all seven CRUD methods. The --model=Product flag enables route model binding – Laravel automatically finds the product by ID instead of you querying it manually.
To better understand how requests are handled, check this Laravel Controllers Guide.
Open app/Http/Controllers/ProductController.php and fill in the methods:
<?php
namespace App\Http\Controllers;
use App\Models\Product;
use Illuminate\Http\Request;
use Illuminate\Http\RedirectResponse;
use Illuminate\View\View;
class ProductController extends Controller
{
// GET /products - list all products
public function index(): View
{
$products = Product::latest()->paginate(10);
return view('products.index', compact('products'));
}
// GET /products/create - show create form
public function create(): View
{
return view('products.create');
}
// POST /products - store new product
public function store(Request $request): RedirectResponse
{
$validated = $request->validate([
'name' => 'required|string|max:255',
'description' => 'nullable|string',
'price' => 'required|numeric|min:0',
'quantity' => 'required|integer|min:0',
]);
Product::create($validated);
return redirect()
->route('products.index')
->with('success', 'Product created successfully.');
}
// GET /products/{product} - show single product
public function show(Product $product): View
{
return view('products.show', compact('product'));
}
// GET /products/{product}/edit - show edit form
public function edit(Product $product): View
{
return view('products.edit', compact('product'));
}
// PUT /products/{product} - update product
public function update(Request $request, Product $product): RedirectResponse
{
$validated = $request->validate([
'name' => 'required|string|max:255',
'description' => 'nullable|string',
'price' => 'required|numeric|min:0',
'quantity' => 'required|integer|min:0',
]);
$product->update($validated);
return redirect()
->route('products.index')
->with('success', 'Product updated successfully.');
}
// DELETE /products/{product} - delete product
public function destroy(Product $product): RedirectResponse
{
$product->delete();
return redirect()
->route('products.index')
->with('success', 'Product deleted successfully.');
}
}
Step 5: Define Routes
One line in routes/web.php registers all seven CRUD routes:
<?php
use App\Http\Controllers\ProductController;
use Illuminate\Support\Facades\Route;
Route::resource('products', ProductController::class);
Verify the routes were created:
php artisan route:list --name=products
Output:
GET|HEAD products .................. products.index › ProductController@index
POST products .................. products.store › ProductController@store
GET|HEAD products/create ........... products.create › ProductController@create
GET|HEAD products/{product} ......... products.show › ProductController@show
PUT|PATCH products/{product} ......... products.update › ProductController@update
DELETE products/{product} ......... products.destroy › ProductController@destroy
GET|HEAD products/{product}/edit .... products.edit › ProductController@edit
Step 6: Create the Blade Views
Create the views directory:
mkdir -p resources/views/products
Create a shared layout in resources/views/layouts/app.blade.php:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Product Manager</title>
<style>
body { font-family: Arial, sans-serif; max-width: 900px; margin: 40px auto; padding: 0 20px; }
table { width: 100%; border-collapse: collapse; margin-top: 20px; }
th, td { padding: 10px; border: 1px solid #ddd; text-align: left; }
th { background: #f5f5f5; }
.btn { padding: 8px 16px; border: none; border-radius: 4px; cursor: pointer; text-decoration: none; font-size: 14px; }
.btn-blue { background: #3498db; color: white; }
.btn-green { background: #27ae60; color: white; }
.btn-red { background: #e74c3c; color: white; }
.alert { padding: 12px; border-radius: 4px; margin-bottom: 20px; }
.alert-success { background: #d4edda; color: #155724; border: 1px solid #c3e6cb; }
.alert-error { background: #f8d7da; color: #721c24; border: 1px solid #f5c6cb; }
input, textarea { width: 100%; padding: 8px; border: 1px solid #ddd; border-radius: 4px; margin: 6px 0 15px; box-sizing: border-box; }
label { font-weight: bold; font-size: 14px; }
</style>
</head>
<body>
<h1>Product Manager</h1>
<nav>
<a href="{{ route('products.index') }}">All Products</a> |
<a href="{{ route('products.create') }}">Add Product</a>
</nav>
<hr>
@if(session('success'))
<div class="alert alert-success">{{ session('success') }}</div>
@endif
@if($errors->any())
<div class="alert alert-error">
<ul>
@foreach($errors->all() as $error)
<li>{{ $error }}</li>
@endforeach
</ul>
</div>
@endif
@yield('content')
</body>
</html>
Index view – resources/views/products/index.blade.php:
@extends('layouts.app')
@section('content')
<table>
<thead>
<tr>
<th>Name</th>
<th>Price</th>
<th>Quantity</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
@forelse($products as $product)
<tr>
<td>{{ $product->name }}</td>
<td>${{ number_format($product->price, 2) }}</td>
<td>{{ $product->quantity }}</td>
<td>
<a href="{{ route('products.show', $product) }}" class="btn btn-blue">View</a>
<a href="{{ route('products.edit', $product) }}" class="btn btn-green">Edit</a>
<form action="{{ route('products.destroy', $product) }}" method="POST" style="display:inline"
onsubmit="return confirm('Delete this product?')">
@csrf
@method('DELETE')
<button type="submit" class="btn btn-red">Delete</button>
</form>
</td>
</tr>
@empty
<tr>
<td colspan="4">No products found. <a href="{{ route('products.create') }}">Add one</a>.</td>
</tr>
@endforelse
</tbody>
</table>
{{ $products->links() }}
@endsection
Create view – resources/views/products/create.blade.php:
@extends('layouts.app')
@section('content')
<h2>Add New Product</h2>
<form action="{{ route('products.store') }}" method="POST">
@csrf
<label>Name</label>
<input type="text" name="name" value="{{ old('name') }}" required>
<label>Description</label>
<textarea name="description" rows="4">{{ old('description') }}</textarea>
<label>Price</label>
<input type="number" name="price" value="{{ old('price') }}" step="0.01" min="0" required>
<label>Quantity</label>
<input type="number" name="quantity" value="{{ old('quantity', 0) }}" min="0" required>
<button type="submit" class="btn btn-green">Save Product</button>
<a href="{{ route('products.index') }}" class="btn btn-blue">Cancel</a>
</form>
@endsection
Edit view – resources/views/products/edit.blade.php:
@extends('layouts.app')
@section('content')
<h2>Edit Product</h2>
<form action="{{ route('products.update', $product) }}" method="POST">
@csrf
@method('PUT')
<label>Name</label>
<input type="text" name="name" value="{{ old('name', $product->name) }}" required>
<label>Description</label>
<textarea name="description" rows="4">{{ old('description', $product->description) }}</textarea>
<label>Price</label>
<input type="number" name="price" value="{{ old('price', $product->price) }}" step="0.01" min="0" required>
<label>Quantity</label>
<input type="number" name="quantity" value="{{ old('quantity', $product->quantity) }}" min="0" required>
<button type="submit" class="btn btn-green">Update Product</button>
<a href="{{ route('products.index') }}" class="btn btn-blue">Cancel</a>
</form>
@endsection
Show view – resources/views/products/show.blade.php:
@extends('layouts.app')
@section('content')
<h2>{{ $product->name }}</h2>
<table>
<tr><th>Field</th><th>Value</th></tr>
<tr><td>Name</td><td>{{ $product->name }}</td></tr>
<tr><td>Description</td><td>{{ $product->description ?? 'N/A' }}</td></tr>
<tr><td>Price</td><td>${{ number_format($product->price, 2) }}</td></tr>
<tr><td>Quantity</td><td>{{ $product->quantity }}</td></tr>
<tr><td>Created</td><td>{{ $product->created_at->format('d M Y') }}</td></tr>
</table>
<br>
<a href="{{ route('products.edit', $product) }}" class="btn btn-green">Edit</a>
<a href="{{ route('products.index') }}" class="btn btn-blue">Back to List</a>
@endsection
Step 7: Test the Application
Start the development server if it isn’t already running:
php artisan serve
Visit these URLs to test each operation:
http://127.0.0.1:8000/products– list all productshttp://127.0.0.1:8000/products/create– add a new producthttp://127.0.0.1:8000/products/1– view product with ID 1http://127.0.0.1:8000/products/1/edit– edit product with ID 1
How It All Connects
The request flow for creating a product:
User fills form at /products/create
→ POST /products
→ Route matches products.store
→ ProductController@store runs
→ $request->validate() checks input
→ Product::create($validated) inserts to database
→ redirect()->route('products.index') sends user back
→ session('success') shows the flash message
Understanding this flow makes every other Laravel feature easier to learn – the same pattern repeats across every resource in every Laravel application.
Common Mistakes and Fixes
Mass assignment exception
// Error: Add [name] to fillable property
// Fix: Add columns to $fillable in the model
protected $fillable = ['name', 'description', 'price', 'quantity'];
CSRF token mismatch
// Every POST, PUT, PATCH, DELETE form needs this inside the form tag
@csrf
// PUT and DELETE forms also need this
@method('PUT') // for update
@method('DELETE') // for delete
View not found error
// Error: View [products.index] not found
// Fix: Check the file exists at exactly this path
resources/views/products/index.blade.php
// Dots map to directory separators
// products.index = resources/views/products/index.blade.php
Route model binding fails with 404
// Make sure the controller was generated with --model flag
php artisan make:controller ProductController --resource --model=Product
// Or add the type hint manually
public function show(Product $product): View {}
Debugging Tips
// Check all registered routes
php artisan route:list
// Check specifically for product routes
php artisan route:list --name=products
// Dump a variable and stop execution
dd($product);
dd($request->all());
// Clear cached config when changes don't take effect
php artisan config:clear
php artisan route:clear
php artisan cache:clear
Frequently Asked Questions
What is CRUD in Laravel?
CRUD stands for Create, Read, Update, Delete – the four basic database operations. In a Laravel CRUD application, Create maps to POST requests that insert new records, Read maps to GET requests that fetch records, Update maps to PUT/PATCH requests that modify records, and Delete maps to DELETE requests that remove records. Laravel’s resource controllers and Route::resource() provide all four operations with a single command each.
What is the difference between Route::resource() and Route::apiResource()?
Route::resource() generates all seven routes including create and edit – the routes that return HTML forms. Route::apiResource() generates five routes, skipping create and edit because APIs don’t serve HTML forms. Use apiResource for JSON API endpoints, resource for traditional web applications.
Why do I need @csrf in Laravel forms?
Laravel automatically verifies a CSRF (Cross-Site Request Forgery) token on all POST, PUT, PATCH, and DELETE requests. The @csrf directive adds a hidden input field containing the token. Without it every form submission returns a 419 “Page Expired” error. This protection prevents malicious sites from tricking users into submitting your forms.
What is route model binding in Laravel?
Route model binding automatically fetches a database record based on the route parameter. When you type-hint a model in a controller method (Product $product), Laravel queries the database for a product with the ID from the URL and passes it directly – or returns a 404 if not found. Without it you’d write $product = Product::findOrFail($id) in every method manually.
How do I add search functionality to the product list?
<?php
// In ProductController@index
public function index(Request $request): View
{
$query = Product::query();
if ($request->filled('search')) {
$query->where('name', 'like', '%' . $request->search . '%');
}
$products = $query->latest()->paginate(10)->withQueryString();
return view('products.index', compact('products'));
}
Add a search form above the table in the index view:
<form method="GET" action="{{ route('products.index') }}">
<input type="text" name="search" value="{{ request('search') }}" placeholder="Search products...">
<button type="submit" class="btn btn-blue">Search</button>
</form>
Summary
A complete Laravel CRUD application requires these pieces working together:
- Model with $fillable – defines which columns can be mass assigned via
create()andupdate() - Migration – defines the database table structure
- Resource controller – seven methods covering every CRUD operation
- Route::resource() – registers all seven routes with one line
- Blade views – index, create, edit, show with @csrf on every form
- Validation in controller –
$request->validate()before every create and update
For understanding the controllers this guide uses in depth, the Laravel controllers guide covers resource controllers, routing, and request handling in detail. For protecting CRUD routes with authentication and role checks, the Laravel middleware guide covers auth middleware, custom middleware, and middleware groups from scratch.
Related Guides
External Resource
For official documentation, refer to Laravel Documentation.
