Laravel CRUD Application: Build a Complete Product Manager Step by Step

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);

Laravel Middleware Explained

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 viewresources/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 viewresources/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 viewresources/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 viewresources/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 products
  • http://127.0.0.1:8000/products/create – add a new product
  • http://127.0.0.1:8000/products/1 – view product with ID 1
  • http://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() and update()
  • 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.

External Resource

For official documentation, refer to Laravel Documentation.

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top