Reader Stacks

Building a Complete CRUD With Search, Image Upload, and Pagination in Laravel

The four pieces of a real CRUD resource — listing with search and pagination, image-handled create/update, and safe delete — assembled into one practical, version-agnostic walkthrough.

A basic Laravel CRUD tutorial usually stops at plain create/read/update/delete — a real admin interface almost always also needs search, pagination, and image upload handled correctly. This walks through all four together, as one connected resource controller.

1. The migration and model

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

2. Listing with search and pagination

public function index(Request $request)
{
    $products = Product::query()
        ->when($request->filled('search'), function ($query) use ($request) {
            $query->where('name', 'like', '%'.$request->search.'%');
        })
        ->latest()
        ->paginate(15)
        ->withQueryString(); // preserves ?search=... across pagination links

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

withQueryString() is the detail most tutorials skip and then wonder why the search term disappears when clicking to page 2 — without it, pagination links only carry the page parameter, silently dropping search and any other query string values from the current request.

3. The search input and pagination links in the view

<form method="GET">
    <input type="text" name="search" value="{{ request('search') }}">
    <button type="submit">Search</button>
</form>

@foreach ($products as $product)
    {{ $product->name }}
@endforeach

{{ $products->links() }}

4. Storing a new record with an image

public function store(Request $request)
{
    $validated = $request->validate([
        'name' => 'required|string|max:255',
        'description' => 'nullable|string',
        'price' => 'required|numeric|min:0',
        'image' => 'nullable|image|max:2048', // max size in kilobytes
    ]);

    if ($request->hasFile('image')) {
        $validated['image'] = $request->file('image')->store('products', 'public');
    }

    Product::create($validated);

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

store('products', 'public') saves the file under storage/app/public/products and returns the relative path — this requires php artisan storage:link to have been run once, which symlinks storage/app/public to public/storage so the uploaded files are actually reachable over HTTP.

5. Updating a record, including replacing its image

public function update(Request $request, Product $product)
{
    $validated = $request->validate([
        'name' => 'required|string|max:255',
        'description' => 'nullable|string',
        'price' => 'required|numeric|min:0',
        'image' => 'nullable|image|max:2048',
    ]);

    if ($request->hasFile('image')) {
        if ($product->image) {
            Storage::disk('public')->delete($product->image); // clean up the old file
        }
        $validated['image'] = $request->file('image')->store('products', 'public');
    }

    $product->update($validated);

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

Deleting the old image file before saving the new path is a step that's easy to forget — without it, replaced images just accumulate as orphaned files in storage indefinitely, never referenced by any record but never cleaned up either.

6. Deleting a record (and its image)

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.');
}

The same cleanup principle applies on delete — removing the database record without also removing its associated file leaves that file orphaned in storage permanently.

Topics: File Uploads & Media Database Queries & Eloquent Pagination & Filtering