In this article, you will know how to make Laravel 12 CRUD with image upload. First you need to follow Laravel 12 CRUD Application Example Tutorial. Then you can follow this tutorial.
Step 1: Update the Database with a New Migration
To develop Laravel 12 image upload CRUD application you need to add image_url column in the product table in the MySQL database to hold the image file path which will used to show the image in the blade views.
Generate Migration
php artisan make:migration add_image_url_to_products_table --table=products
Edit Migration File
database/migrations/2025_02_22_xxxxxx_add_image_url_to_products_table.php)
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up()
{
Schema::table('products', function (Blueprint $table) {
$table->string('image_url')->nullable()->after('price');
});
}
public function down()
{
Schema::table('products', function (Blueprint $table) {
$table->dropColumn('image_url');
});
}
};
Run Migration
php artisan migrate
When you migrate you will have empty column image_url in every row and you find your all previously data unchanged and still there! Isn’t it great?

Step 2: Update the Product Model
Edit app/Models/Product.php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Product extends Model
{
protected $fillable = ['name', 'description', 'price', 'image_url'];
}
Step 3: Update the Controller
Modify app/Http/Controllers/ProductController.php
namespace App\Http\Controllers;
use App\Models\Product;
use Illuminate\Http\Request;
class ProductController extends Controller
{
public function index()
{
$products = Product::all();
return view('products.index', compact('products'));
}
public function create()
{
return view('products.create');
}
public function store(Request $request)
{
$request->validate([
'name' => 'required|string|max:255',
'price' => 'required|numeric|min:0',
'description' => 'nullable|string',
'image' => 'nullable|image|max:2048',
]);
$data = $request->all();
if ($request->hasFile('image')) {
$image = $request->file('image');
$imageName = time() . '.' . $image->getClientOriginalExtension();
$image->move(public_path('assets'), $imageName);
$data['image_url'] = 'assets/' . $imageName;
}
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(Request $request, Product $product)
{
$request->validate([
'name' => 'required|string|max:255',
'price' => 'required|numeric|min:0',
'description' => 'nullable|string',
'image' => 'nullable|image|max:2048',
]);
$data = $request->all();
if ($request->hasFile('image')) {
if ($product->image_url && file_exists(public_path($product->image_url))) {
unlink(public_path($product->image_url));
}
$image = $request->file('image');
$imageName = time() . '.' . $image->getClientOriginalExtension();
$image->move(public_path('assets'), $imageName);
$data['image_url'] = 'assets/' . $imageName;
}
$product->update($data);
return redirect()->route('products.index')->with('success', 'Product updated successfully.');
}
public function destroy(Product $product)
{
if ($product->image_url && file_exists(public_path($product->image_url))) {
unlink(public_path($product->image_url));
}
$product->delete();
return redirect()->route('products.index')->with('success', 'Product deleted successfully.');
}
}
Step 4: Create public/assets and Add Placeholder
- Create the public/assets directory if it doesn’t exist.
- Add product_default.jpg (a placeholder image) to public/assets/.
Step 5: Update Blade Views
resources/views/products/index.blade.php (50% Image Size)
@extends('layouts.app')
@section('content')
<h1>Products</h1>
@if (session('success'))
<div class="alert-success">{{ session('success') }}</div>
@endif
<a href="{{ route('products.create') }}" class="btn">Add Product</a>
<table class="table">
<thead>
<tr>
<th>ID</th>
<th>Name</th>
<th>Price</th>
<th>Image</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
@foreach ($products as $product)
<tr>
<td>{{ $product->id }}</td>
<td>{{ $product->name }}</td>
<td>{{ $product->price }}</td>
<td>
<img src="{{ $product->image_url ? asset($product->image_url) : asset('assets/product_default.jpg') }}"
alt="{{ $product->name }}"
style="width: 50%; height: 50%;">
</td>
<td>
<a href="{{ route('products.show', $product) }}" class="btn">View</a>
<a href="{{ route('products.edit', $product) }}" class="btn">Edit</a>
<form action="{{ route('products.destroy', $product) }}" method="POST" style="display:inline;">
@csrf
@method('DELETE')
<button type="submit" class="btn btn-danger" onclick="return confirm('Are you sure?')">Delete</button>
</form>
</td>
</tr>
@endforeach
</tbody>
</table>
@endsection
resources/views/products/create.blade.php (Full-Size Preview)
@extends('layouts.app')
@section('content')
<h1>Create Product</h1>
<form action="{{ route('products.store') }}" method="POST" enctype="multipart/form-data">
@csrf
<div class="form-group">
<label>Name</label>
<input type="text" name="name" class="form-control" required>
</div>
<div class="form-group">
<label>Description</label>
<textarea name="description" class="form-control"></textarea>
</div>
<div class="form-group">
<label>Price</label>
<input type="number" name="price" class="form-control" step="0.01" required>
</div>
<div class="form-group">
<label>Image</label>
<input type="file" name="image" class="form-control" accept="image/*" onchange="previewImage(event)">
<img id="image-preview" src="{{ asset('assets/product_default.jpg') }}" alt="Preview" style="display: block; margin-top: 10px;">
</div>
<button type="submit" class="btn">Save</button>
<a href="{{ route('products.index') }}" class="btn btn-secondary">Cancel</a>
</form>
<script>
function previewImage(event) {
const preview = document.getElementById('image-preview');
preview.src = event.target.files[0] ? URL.createObjectURL(event.target.files[0]) : "{{ asset('assets/product_default.jpg') }}";
}
</script>
@endsection
resources/views/products/edit.blade.php (Full-Size Preview)
@extends('layouts.app')
@section('content')
<h1>Edit Product</h1>
<form action="{{ route('products.update', $product) }}" method="POST" enctype="multipart/form-data">
@csrf
@method('PUT')
<div class="form-group">
<label>Name</label>
<input type="text" name="name" class="form-control" value="{{ $product->name }}" required>
</div>
<div class="form-group">
<label>Description</label>
<textarea name="description" class="form-control">{{ $product->description }}</textarea>
</div>
<div class="form-group">
<label>Price</label>
<input type="number" name="price" class="form-control" step="0.01" value="{{ $product->price }}" required>
</div>
<div class="form-group">
<label>Image</label>
<input type="file" name="image" class="form-control" accept="image/*" onchange="previewImage(event)">
<p>Current Image:</p>
<img id="image-preview" src="{{ $product->image_url ? asset($product->image_url) : asset('assets/product_default.jpg') }}"
alt="Product Image" style="margin-top: 10px;">
</div>
<button type="submit" class="btn">Update</button>
<a href="{{ route('products.index') }}" class="btn btn-secondary">Cancel</a>
</form>
<script>
function previewImage(event) {
const preview = document.getElementById('image-preview');
preview.src = event.target.files[0] ? URL.createObjectURL(event.target.files[0]) : "{{ $product->image_url ? asset($product->image_url) : asset('assets/product_default.jpg') }}";
}
</script>
@endsection
resources/views/products/show.blade.php (Full-Size Image)
@extends('layouts.app')
@section('content')
<h1>Product Details</h1>
<p><strong>Name:</strong> {{ $product->name }}</p>
<p><strong>Description:</strong> {{ $product->description ?? 'N/A' }}</p>
<p><strong>Price:</strong> ${{ $product->price }}</p>
<p><strong>Image:</strong></p>
<img src="{{ $product->image_url ? asset($product->image_url) : asset('assets/product_default.jpg') }}"
alt="{{ $product->name }}">
<a href="{{ route('products.index') }}" class="btn">Back</a>
@endsection
Step 6: Test the Application
Run the Server
php artisan serve
Verify:
- List View: Images or product_default.jpg at 20% size.
- Create: Full-size preview with product_default.jpg initially.
- Edit: Full-size current image or product_default.jpg, with preview on new upload.
- Show: Full-size image or product_default.jpg.
Hope, this tutorial help you have a Laravel image upload CRUD application.