How to Build a Laravel 11 CRUD from Scratch for Beginners

You installed Laravel, the welcome page loads, and now you are not sure what to build first. Many online tutorials mix Laravel 8 conventions with newer ones or pull in packages you...

How to Build a Laravel 11 CRUD from Scratch for Beginners

You installed Laravel, the welcome page loads, and now you are not sure what to build first. Many online tutorials mix Laravel 8 conventions with newer ones or pull in packages you do not need yet. The result is a half-working CRUD and errors you cannot explain.

This guide builds a complete product CRUD in Laravel 11 in order: table, model, controller, validation, views, search, and deletion. Each step explains why it exists, so you can repeat the pattern for customers, categories, or orders without copying blindly.

Setup

You need PHP 8.2+, Composer, and a database. If you are still assembling your toolkit, see 15 free tools for beginner web developers.

composer create-project laravel/laravel shop
cd shop
php artisan serve

Laravel 11 defaults to SQLite, which is perfect for learning. To use MySQL or PostgreSQL, change DB_CONNECTION and the credentials in .env and create the empty database first.

The CRUD Map

ActionHTTP methodURLController method
ListGET/productsindex
Create formGET/products/createcreate
StorePOST/productsstore
ShowGET/products/{product}show
Edit formGET/products/{product}/editedit
UpdatePUT/PATCH/products/{product}update
DeleteDELETE/products/{product}destroy

Following this resource convention means one route line covers all seven actions.

Step 1: Migration and Model

php artisan make:model Product -mf
public function up(): void
{
    Schema::create('products', function (Blueprint $table) {
        $table->id();
        $table->string('name');
        $table->decimal('price', 10, 2);
        $table->unsignedInteger('stock')->default(0);
        $table->text('description')->nullable();
        $table->timestamps();
    });
}

Migrations keep your schema in Git, so teammates and servers get the exact same structure with php artisan migrate. Prices use decimal because floats introduce rounding errors with money.

class Product extends Model
{
    use HasFactory;

    protected $fillable = ['name', 'price', 'stock', 'description'];
}

$fillable blocks mass assignment: without it, someone could add extra fields to a form and overwrite columns you never meant to expose.

Step 2: Controller and Routes

php artisan make:controller ProductController --resource --model=Product
php artisan make:request StoreProductRequest
// routes/web.php
use App\Http\Controllers\ProductController;

Route::resource('products', ProductController::class);

Step 3: Validation with a Form Request

public function authorize(): bool
{
    return true; // replace with a real permission check later
}

public function rules(): array
{
    return [
        'name'        => 'required|string|max:255',
        'price'       => 'required|numeric|min:0',
        'stock'       => 'required|integer|min:0',
        'description' => 'nullable|string|max:2000',
    ];
}

The generated authorize() returns false. Forget to change it and every submit ends in a 403.

Step 4: Controller Logic

public function index(Request $request)
{
    $products = Product::query()
        ->when($request->q, fn ($query, $q) => $query->where('name', 'like', "%{$q}%"))
        ->latest()
        ->paginate(10)
        ->withQueryString();

    return view('products.index', compact('products'));
}

public function store(StoreProductRequest $request)
{
    Product::create($request->validated());

    return redirect()->route('products.index')->with('success', 'Product created.');
}

public function update(StoreProductRequest $request, Product $product)
{
    $product->update($request->validated());

    return redirect()->route('products.index')->with('success', 'Product updated.');
}

public function destroy(Product $product)
{
    $product->delete();

    return back()->with('success', 'Product deleted.');
}

The create and edit methods simply return their views. Product $product is route model binding: Laravel loads the record by ID and returns 404 automatically if it does not exist. validated() contains only fields that passed your rules, and withQueryString() keeps the search term across pages.

Step 5: Blade Views

Create and edit share one partial, products/_form.blade.php:

@csrf
<label>Name</label>
<input name="name" value="{{ old('name', $product->name ?? '') }}">
@error('name') <small>{{ $message }}</small> @enderror

<label>Price</label>
<input type="number" step="0.01" name="price" value="{{ old('price', $product->price ?? '') }}">

<label>Stock</label>
<input type="number" name="stock" value="{{ old('stock', $product->stock ?? 0) }}">

<button type="submit">Save</button>

Browsers only send GET and POST, so the edit form spoofs PUT and the delete button spoofs DELETE:

<form method="POST" action="{{ route('products.update', $product) }}">
    @method('PUT')
    @include('products._form')
</form>

<form method="POST" action="{{ route('products.destroy', $product) }}"
      onsubmit="return confirm('Delete this product?')">
    @csrf
    @method('DELETE')
    <button type="submit">Delete</button>
</form>

old() refills the form after a failed validation so users do not retype everything. Put {{ $products->links() }} under the list table for pagination.

Step 6: The List Page and Flash Messages

The index.blade.php view shows the session message, a search box, and the product table:

@if (session('success'))
    <div class="alert">{{ session('success') }}</div>
@endif

<form method="GET" action="{{ route('products.index') }}">
    <input name="q" value="{{ request('q') }}" placeholder="Search products">
</form>

<table>
@forelse ($products as $product)
    <tr>
        <td>{{ $product->name }}</td>
        <td>${{ number_format($product->price, 2) }}</td>
        <td>{{ $product->stock }}</td>
        <td><a href="{{ route('products.edit', $product) }}">Edit</a></td>
    </tr>
@empty
    <tr><td colspan="4">No products yet.</td></tr>
@endforelse
</table>

{{ $products->links() }}

@forelse handles the empty state, so new users see a message instead of a blank page. Double curly braces escape HTML automatically, so a product name containing a script tag will not run.

Step 7: Seed Sample Data with a Factory

Pagination and search problems never show up with two rows. Fill in the factory created in step 1:

// database/factories/ProductFactory.php
public function definition(): array
{
    return [
        'name'  => fake()->words(2, true),
        'price' => fake()->randomFloat(2, 1, 250),
        'stock' => fake()->numberBetween(0, 100),
    ];
}

Run php artisan tinker and type Product::factory()->count(50)->create();. Now you can try search and paging with realistic volume. The same factory will later power your automated tests.

Common Errors and Fixes

ErrorCauseFix
419 Page ExpiredMissing @csrf or expired sessionAdd @csrf to every POST form
MassAssignmentExceptionColumn not in $fillableAdd it to $fillable
403 This action is unauthorizedauthorize() still returns falseReturn true or a real check
405 Method Not AllowedMissing @method('PUT') or @method('DELETE')Add the method directive
Target class does not existController not imported in routesAdd the correct use line
Connection refused / no such tableWrong database settings or migrations not runCheck .env, run config:clear and migrate

How to read an error page

When Laravel shows its error page, do not paste the whole thing into a search engine. Read the first line for the error type, then find the first stack frame that points to your own code under app/ or resources/views/. That is usually where the typo, missing import, or unpassed variable lives. The same error is also written to storage/logs/laravel.log if you closed the page.

Checklist for a Usable CRUD

  • All input validated in a Form Request, including updates.
  • Every form has @csrf; edit and delete forms have @method.
  • Success messages after create, update, and delete.
  • The list is paginated and searchable.
  • Deletion asks for confirmation.
  • CRUD routes sit behind the auth middleware before you deploy.

Where to Go Next

Once a single-table CRUD feels easy, add relationships such as products belonging to categories, and sketch them first with a simple ERD. Then review web application security best practices before real users touch the app. You will reuse these seven actions in nearly every project, from point-of-sale apps to inventory systems.

Yudhi
Written by
Yudhi
Founder & Lead Developer, GudangCode

Yudhi is the founder of GudangCode and a Laravel developer who has built dozens of ready-to-use business information systems — from POS and HRIS to management apps. He writes guides and articles on GudangCode to help Indonesian developers run, understand, and deploy Laravel source code correctly.

LaravelPHPMySQLSistem Informasi Bisnis See all articles by Yudhi
Want the full source code & apps?

Sign up free to download ready-to-use business applications, information systems, and Laravel source code.

Sign Up Free & Download
Laravel PHP CRUD Tutorial
Share this article
Back to Blog
📚 Free Learning Hub

Learn Coding for Free at DhieCoderWeb

Explore Laravel, PHP, JavaScript tutorials, source code, web development guides, and practical programming tips.

DhieCoderWeb
100+
Tutorials
Free
Learning
SEO
Tips
Visit Dhiecoderweb.com →

Get Full Access Now!

Join our membership and unlock exclusive access to all premium features. Fast, easy, and ready to use instantly.

Join Membership Now