5 levels of handling images in Laravel

Image uploads are a common feature in most web applications. In this post, we'll walk through how to implement image uploads in Laravel, starting from the basics and gradually moving toward a full-featured solution. We'll cover resizing, image optimization, and UI enhancements and show how Spatie's packages and products can make the whole process easier, more efficient, and even a bit fun.
Level 1: Plain Laravel and the basics of image uploads
Let's start with a minimal example of how you can handle image uploads in Laravel using built-in functionality—no packages, just core features.
1. Set up the form
First, you'll need a form in your Blade template that allows users to upload an image:
<form
action="{{ route('upload') }}"
method="POST"
enctype="multipart/form-data"
>
@csrf
<input type="file" name="image">
<button type="submit">Upload</button>
</form>
2. Handle the upload in a controller
In your controller, you can handle the incoming file like this:
public function store(Request $request)
{
$request->validate([
'image' => 'required|image|max:2048', // max 2MB
]);
$path = $request->file('image')->store('uploads', 'public');
return back()
->with('success', 'Image uploaded successfully!')
->with('path', $path);
}
What you get
✅ A working upload flow
✅ Basic validation (image file, max size)
✅ Storage in the public disk
What's missing?
❌ No resizing or manipulation
❌ No optimization for performance
❌ No UI enhancements or previews
❌ No support for attaching images to models (like users or posts)
Level 2: Adding spatie/image
Now that basic uploads are working, let's say you want to resize or crop images before saving them. Laravel doesn't offer built-in tools for this, but Spatie's image package gives you an elegant and straightforward way to manipulate images.
Using the image package, we can extend our controller to add some image resizing:
public function store(Request $request)
{
$request->validate([
'image' => 'required|image|max:2048',
]);
$uploadedFile = $request->file('image');
// Store the original temporarily
$path = $uploadedFile->store('temp');
// Define the final path
$finalPath = 'uploads/' . $uploadedFile->hashName();
// Resize and save
Image::load(storage_path('app/' . $path))
->width(800)
->save(storage_path('app/public/' . $finalPath));
// Optionally remove the original
Storage::delete($path);
return back()
->with('success', 'Image resized and uploaded!')
->with('path', $finalPath);
}
Other things you can do
- height(400) – constrain height
- fit(800, 600) – resize to fit into a box
- crop(width, height, x, y) – manually crop a section
- blur(15) – apply blur
- greyscale() – make the image black and white
See the full list of methods in the spatie/image docs.
What you get
✅ Image resizing and manipulation
✅ Clean, chainable syntax
✅ Works well for one-off or simple workflows
What's missing?
❌ No optimization (file size can still be large)
❌ Still manual—no model integration
❌ No UI integration
Next, let's fix the performance part by optimizing the images before we store them.
Level 3: Using spatie/image-optimizer to shrink file size
So far, we've got image uploads and resizing working, but the file size might still be much larger than it needs to be. Especially for web apps, keeping images small is critical for performance. That’s where spatie/image-optimizer comes in.
This package automatically optimizes images using well-known CLI tools under the hood (like jpegoptim, pngquant, and svgo)—without noticeably degrading quality.
Lucky for us image-optimizer seemlesly integrates with the image package so we only need to add one line to our controller:
public function store(Request $request)
{
...
// Resize and save
Image::load(storage_path('app/' . $path))
->width(800)
->optimize() // This will optimize the image before saving.
->save(storage_path('app/public/' . $finalPath));
...
}
What you get
✅ Automatically smaller file sizes
✅ No visual quality loss
✅ Works with any image manipulation flow
What's missing?
❌ Still no model integration
❌ No UI integration
Level 4: Using spatie/laravel-medialibrary
Spatie's laravel-medialibrary is a powerful package that takes image uploads to the next level. It allows you to associate uploaded files (not just images!) with Eloquent models, generate automatic conversions, handle storage cleanly, and much more.
Installation
You can find the full installation instructions for laravel-media library in our docs.
Usage
Once installed, we can create a model, in this case a post, and simply attach our uploaded image to it:
public function store(Request $request)
{
$request->validate([
'title' => 'required',
'image' => 'required|image|max:2048',
]);
$post = Post::create([
'title' => $request->input('title'),
]);
$post->addMediaFromRequest('image')
->toMediaCollection('featured');
return redirect()->back()
->with('success', 'Post created and image uploaded!');
}
What you get
✅ Images are attached to models
✅ Automatic conversions (thumbs, responsive, etc.)
✅ Clean file storage
✅ Support for collections and multiple images
What's missing?
❌ No user-friendly upload UI
❌ Upload experience is still native HTML form elements (no previews, drag & drop)
Level 5: Using Media Library Pro for a polished UI uxperience
Once you've got laravel-medialibrary set up, Media Library Pro takes things to a whole new level. It's a commercial add-on by Spatie that provides drop-in Vue and Livewire components for a modern, user-friendly image upload experience—with previews, drag & drop, progress bars, reordering, and more.
💡 Media Library Pro is a paid product, but it will save you hours of frontend work and integrates tightly with Laravel.
Media Library Pro is available for all types of frontend: Blade, Livewire, React and Vue. Installation instructions can be found in our extensive documentation.
Once installed, you can add the component to your frontend code. Here's a Blade example:
<form method="post">
<x-media-library-attachment name= "image" rules= "mimes:png,jpeg,pdf"/>
<button>Submit</button>
</form
And in our controller you can handle the upload like this:
public function store(Request $request)
{
$request->validate([
'title' => 'required',
'image' => 'required|image|max:2048',
]);
$post = Post::create([
'title' => $request->input('title'),
]);
$post->addFromMediaLibraryRequest('image')
->toMediaCollection('featured');
return redirect()->back()
->with('success', 'Post created and image uploaded!');
}
Which should result in a beautiful, functional upload component:
What you get
✅ Modern, reactive UI components
✅ Deep integration with your models
✅ Preview, progress, reordering out of the box
✅ Built and maintained by the creators of Media Library
What's missing?
🙌 Nothing!
Conclusion
Whether you stick with Laravel's built-in tools or go all-in with Media Library Pro, how far you take your image upload implementation depends on your project's needs. The Spatie packages and products at every level offer well-crafted solutions to help you build a clean, efficient, and scalable solution.