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)
AI-Powered
TL;DR — Quick Summary
Click Generate Summary to get an AI-powered TL;DR of this article.
Gemini is reading the article...
    Could not generate summary. Please try again.

    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.