Laravel

Building a Complete CRUD Application in Laravel 13: A Step-by-Step Guide (Updated for Laravel 11/12/13)

Building a Complete CRUD Application in Laravel 13: A Step-by-Step Guide (Updated for Laravel 11/12/13)

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-app

Start 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 migrate

Step 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 -mcr

The `-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 migrate

Step 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 UpdateProductRequest

In 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>
@endsection

create.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>
@endsection

For 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:link

Step 12: Test Your Application

Start the server:

php artisan serve

Visit `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.

Written by

Hupen Pun

Dedicated to sharing valuable insights, tech tutorials, and educational resources to help you level up your skills.