Menü schliessen
Created: March 31st 2026
Categories: IT Development,  Laravel,  Laravel Package
Author: Milos Jevtic

Spatie Media Library: What It Does, How It Works, and Why You Need It

Introduction: Why Is File Handling in Laravel So Painful?

You're building a Laravel app. Users need to upload a profile photo. Simple enough —
you grab the file from the request, store it somewhere, save the path in the database,
and move on.

Then your client asks for thumbnails. Then a second upload field for a cover image.
Then the mobile app needs URLs in API responses. Then someone wants to replace a file
without orphaning the old one. Suddenly your "simple" file handling is a mess of
custom columns, manual path building, and one-off image resize jobs.

This is the problem Spatie Media Library solves. Instead of managing files yourself,
you attach them to your Eloquent models and let the package handle storage, retrieval,
conversions, and cleanup.

In this guide we'll cover what the package actually does, how it works under the hood,
when you should use it, and how to get started with the features you'll use every day.

What Is Spatie Media Library?

Spatie Media Library (spatie/laravel-medialibrary) is an open source Laravel package
that lets you associate files of any kind — images, PDFs, videos, documents — with
your Eloquent models through a clean, consistent API.

Instead of storing file paths scattered across dozens of database columns, the package
uses a single media table to track every file, which model it belongs to, and where
it's stored. Your models stay clean, and the package handles the rest.

  • Associate any file with any model - A User has an avatar, a Product has images, a Post has attachments — all handled the same way
  • Collections - Organize media into named groups per model (avatar, gallery, documents)
  • Conversions - Automatically generate thumbnails and resized versions of images when a file is uploaded
  • Fluent API - Add, retrieve, and delete media with expressive, readable code
  • Filesystem agnostic - Works with local storage, S3, and any Laravel-compatible filesystem
  • API friendly - Easily include media URLs in your JSON responses

Why Choose Spatie Media Library?

Before installing any package it's worth asking: do I actually need this, or can I
handle it myself?

Here's an honest comparison.

Doing it yourself

// Storing a file manually
$path = $request->file('avatar')->store('avatars', 'public');
$user->update(['avatar_path' => $path]);

// Getting the URL
$url = Storage::disk('public')->url($user->avatar_path);

// Deleting and replacing
if ($user->avatar_path) {
    Storage::disk('public')->delete($user->avatar_path);
}
$newPath = $request->file('avatar')->store('avatars', 'public');
$user->update(['avatar_path' => $newPath]);

This works fine for a single file. But it gets messy fast:

  • Every new file type needs a new database column (avatar_path, cover_path, resume_path...)
  • You write the same store/delete/replace logic over and over
  • Generating thumbnails means custom jobs and more columns (avatar_thumb_path)
  • Returning URLs in API responses requires manual mapping
  • Old files often get orphaned when replaced

Using Spatie Media Library

// Storing a file
$user->addMediaFromRequest('avatar')->toMediaCollection('avatar');

// Getting the URL
$user->getFirstMediaUrl('avatar');

// Replacing (old file is automatically removed)
$user->clearMediaCollection('avatar');
$user->addMediaFromRequest('avatar')->toMediaCollection('avatar');

Same result, but the package handles the filesystem operations, tracks everything in
one media table, and cleans up old files automatically.

Use Spatie Media Library when:

  • Multiple models in your app need file uploads
  • You need image conversions or thumbnails
  • You want to keep your database schema clean (no file path columns)
  • You're building an API that returns media URLs
  • You want to avoid writing the same file management code repeatedly

Stick with manual file handling when:

  • You only have one simple upload in the entire app and it will never grow
  • You need very custom storage logic the package doesn't support

For most real projects, the package is worth it from the second model that needs a file.

Requirements and Installation

The package requires PHP 8.2 or higher and Laravel 10 or higher.

Install via Composer:

composer require spatie/laravel-medialibrary

Publish and run the migration. This creates the media table that stores all file
records:

php artisan vendor:publish --provider="Spatie\MediaLibrary\MediaLibraryServiceProvider" --tag="medialibrary-migrations"
php artisan migrate

Optionally publish the config file if you want to customize storage disk, file size
limits, or queue settings:

php artisan vendor:publish --provider="Spatie\MediaLibrary\MediaLibraryServiceProvider" --tag="medialibrary-config"

Setting Up the Storage Disk

By default the package stores files on Laravel's public disk. For most beginner
projects that's fine. Make sure you've run:

php artisan storage:link

This creates the symlink from public/storage to storage/app/public so uploaded
files are web-accessible. If you skip this step your file URLs won't work.

Preparing Your Model

To attach media to a model, add the HasMedia interface and the InteractsWithMedia
trait to it. That's all the setup required on the model side.

Let's use a Post model as our example:

// app/Models/Post.php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Spatie\MediaLibrary\HasMedia;
use Spatie\MediaLibrary\InteractsWithMedia;

class Post extends Model implements HasMedia
{
    use InteractsWithMedia;

    protected $fillable = ['title', 'body'];
}

Notice there are no file path columns on the model or in its migration. The media
table handles all of that separately. Your posts table stays clean.

Adding Media to a Model

Once your model implements HasMedia, you can attach files to it in several ways.

From a Request (Most Common)

// In your controller
public function store(Request $request)
{
    $request->validate([
        'title'  => 'required|string',
        'image'  => 'required|image|max:2048',
    ]);

    $post = Post::create(['title' => $request->title, 'body' => $request->body]);

    $post->addMediaFromRequest('image')
         ->toMediaCollection('featured-image');

    return response()->json($post, 201);
}
  • addMediaFromRequest('image') - Grabs the uploaded file from the request by field name
  • toMediaCollection('featured-image') - Stores it under a named collection on this model

From a Local Path

Useful when processing files that aren't directly uploaded by users:

$post->addMedia('/path/to/file.pdf')
     ->toMediaCollection('attachments');

From a URL

Download and attach a remote file directly:

$post->addMediaFromUrl('https://example.com/image.jpg')
     ->toMediaCollection('featured-image');

Retrieving Media

Getting files back out is just as straightforward.

// Get the URL of the first file in a collection
$url = $post->getFirstMediaUrl('featured-image');

// Get the first Media object (with full details)
$media = $post->getFirstMedia('featured-image');
echo $media->file_name;   // image.jpg
echo $media->mime_type;   // image/jpeg
echo $media->size;        // 204800 (bytes)
echo $media->getUrl();    // https://yourapp.com/storage/1/image.jpg

// Get all media in a collection
$allMedia = $post->getMedia('featured-image');

foreach ($allMedia as $media) {
    echo $media->getUrl();
}

Understanding Collections

Collections are named groups of media on a model. Think of them as labeled slots:
a Post might have a featured-image collection (one image) and an attachments
collection (multiple files).

By default every collection accepts any number of files and any file type. You define
and constrain collections in a registerMediaCollections method on your model:

// app/Models/Post.php

use Spatie\MediaLibrary\MediaCollections\Models\Media;

public function registerMediaCollections(): void
{
    // Single file collection — new uploads replace the existing one
    $this->addMediaCollection('featured-image')
         ->singleFile();

    // Multiple files, images only
    $this->addMediaCollection('gallery')
         ->acceptsMimeTypes(['image/jpeg', 'image/png', 'image/webp']);

    // Any file type, no restrictions
    $this->addMediaCollection('attachments');
}

The singleFile() method is particularly useful — when you call addMediaFromRequest
on a single file collection, the old file is automatically deleted before the new one
is stored. No orphaned files, no manual cleanup.

Image Conversions and Thumbnails

This is where the package really earns its place. Conversions let you automatically
generate resized versions of images when a file is added to a collection.

Define conversions in a registerMediaConversions method on your model:

// app/Models/Post.php

use Spatie\MediaLibrary\MediaCollections\Models\Media;

public function registerMediaConversions(?Media $media = null): void
{
    $this->addMediaConversion('thumb')
         ->width(300)
         ->height(300);

    $this->addMediaConversion('banner')
         ->width(1200)
         ->height(400);
}

When you add an image to any collection on this model, the package automatically
generates a thumb (300x300) and a banner (1200x400) version in the background.

Access a specific conversion by passing its name as the second argument:

// Original image
$post->getFirstMediaUrl('featured-image');

// Thumbnail version
$post->getFirstMediaUrl('featured-image', 'thumb');

// Banner version
$post->getFirstMediaUrl('featured-image', 'banner');

Each conversion is stored as a separate file alongside the original. You don't manage
any of this manually — the package handles generation and storage automatically.

Conversions Are Queued by Default

Image conversions run on a queue to avoid slowing down your HTTP requests. Make sure
your queue worker is running during development:

php artisan queue:work

If you want a conversion to run synchronously (immediately, not on a queue) — for
example during tests — add nonQueued():

$this->addMediaConversion('thumb')
     ->width(300)
     ->height(300)
     ->nonQueued();

Media in API Responses

When building an API you'll want media URLs included in your JSON responses. The
cleanest way is to add a computed attribute to your model or API resource.

Using an Accessor on the Model

// app/Models/Post.php

use Illuminate\Database\Eloquent\Casts\Attribute;

protected function featuredImageUrl(): Attribute
{
    return Attribute::get(
        fn () => $this->getFirstMediaUrl('featured-image') ?: null
    );
}

protected function thumbnailUrl(): Attribute
{
    return Attribute::get(
        fn () => $this->getFirstMediaUrl('featured-image', 'thumb') ?: null
    );
}
// Usage in a controller
return response()->json([
    'id'                => $post->id,
    'title'             => $post->title,
    'featured_image_url' => $post->featured_image_url,
    'thumbnail_url'     => $post->thumbnail_url,
]);

Using an API Resource

For larger projects, API Resources are the cleaner approach:

// app/Http/Resources/PostResource.php

namespace App\Http\Resources;

use Illuminate\Http\Resources\Json\JsonResource;

class PostResource extends JsonResource
{
    public function toArray($request): array
    {
        return [
            'id'                 => $this->id,
            'title'              => $this->title,
            'body'               => $this->body,
            'featured_image_url' => $this->getFirstMediaUrl('featured-image') ?: null,
            'thumbnail_url'      => $this->getFirstMediaUrl('featured-image', 'thumb') ?: null,
            'gallery'            => $this->getMedia('gallery')->map(fn ($m) => [
                'url'       => $m->getUrl(),
                'thumb_url' => $m->getUrl('thumb'),
            ]),
        ];
    }
}

Real-World Scenarios

Scenario 1: User Profile with Avatar

A User model with a single avatar that gets replaced when updated:

// app/Models/User.php

public function registerMediaCollections(): void
{
    $this->addMediaCollection('avatar')->singleFile();
}

public function registerMediaConversions(?Media $media = null): void
{
    $this->addMediaConversion('thumb')->width(100)->height(100);
    $this->addMediaConversion('medium')->width(300)->height(300);
}
// In your controller
public function updateAvatar(Request $request)
{
    $request->validate(['avatar' => 'required|image|max:1024']);

    // Because the collection is singleFile(), the old avatar is deleted automatically
    $request->user()
        ->addMediaFromRequest('avatar')
        ->toMediaCollection('avatar');

    return response()->json([
        'avatar_url' => $request->user()->getFirstMediaUrl('avatar', 'medium'),
    ]);
}

Scenario 2: Product with Multiple Gallery Images

A Product model where you can upload several gallery images, each with a thumbnail:

// app/Models/Product.php

public function registerMediaCollections(): void
{
    $this->addMediaCollection('gallery')
         ->acceptsMimeTypes(['image/jpeg', 'image/png', 'image/webp']);
}

public function registerMediaConversions(?Media $media = null): void
{
    $this->addMediaConversion('thumb')->width(200)->height(200);
    $this->addMediaConversion('preview')->width(800)->height(600);
}
// Adding multiple images at once
public function uploadGallery(Request $request, Product $product)
{
    $request->validate(['images.*' => 'required|image|max:4096']);

    foreach ($request->file('images') as $image) {
        $product->addMedia($image)->toMediaCollection('gallery');
    }

    return response()->json([
        'gallery' => $product->getMedia('gallery')->map(fn ($m) => [
            'id'    => $m->id,
            'url'   => $m->getUrl(),
            'thumb' => $m->getUrl('thumb'),
        ]),
    ]);
}

Scenario 3: Deleting a Specific Media Item

When a user removes a specific image from a gallery:

use Spatie\MediaLibrary\MediaCollections\Models\Media;

public function deleteMedia(Request $request, Product $product, Media $media)
{
    // Make sure the media belongs to this product
    abort_if($media->model_id !== $product->id, 403);

    $media->delete(); // Also deletes the file from storage and all conversions

    return response()->json(['message' => 'Image removed.']);
}

Common Mistakes to Avoid

1. Forgetting storage:link

Files get stored but URLs return 404s. Always run this after setting up a new project:

php artisan storage:link

2. Not Running the Queue Worker

You upload an image but conversions never appear. This is almost always because the
queue worker isn't running.

Bad:

# No worker running — conversions silently queue up and never process
php artisan serve

Good:

# Run these in two separate terminals during development
php artisan serve
php artisan queue:work

Or use nonQueued() on your conversions during local development to skip the queue entirely.

3. Storing File Paths in Your Own Database Columns

The whole point of the package is that it manages file storage. Don't duplicate this.

Bad:

// Adding a media column to your own table defeats the purpose
$post->update(['image_path' => $post->getFirstMediaUrl('featured-image')]);

Good:

// Always retrieve URLs directly from the media library
$post->getFirstMediaUrl('featured-image');

4. Calling getFirstMediaUrl() in a Loop Without Eager Loading

This causes an N+1 query problem — one database query per model.

Bad:

$posts = Post::all();

foreach ($posts as $post) {
    echo $post->getFirstMediaUrl('featured-image'); // Queries DB every iteration
}

Good:

$posts = Post::with('media')->get(); // Load all media in one query

foreach ($posts as $post) {
    echo $post->getFirstMediaUrl('featured-image'); // Uses cached result
}

Always use ->with('media') when loading collections of models that have media.

5. Not Validating File Uploads

Never pass uploaded files to the media library without validating them first.

Bad:

$post->addMediaFromRequest('image')->toMediaCollection('featured-image');

Good:

$request->validate(['image' => 'required|image|max:2048|mimes:jpg,jpeg,png,webp']);
$post->addMediaFromRequest('image')->toMediaCollection('featured-image');

Quick Reference

Setup

# Install
composer require spatie/laravel-medialibrary

# Publish and run migrations
php artisan vendor:publish --provider="Spatie\MediaLibrary\MediaLibraryServiceProvider" --tag="medialibrary-migrations"
php artisan migrate

# Create storage symlink
php artisan storage:link

Model Setup

class Post extends Model implements HasMedia
{
    use InteractsWithMedia;
}

Adding Media

$model->addMediaFromRequest('field')->toMediaCollection('collection');
$model->addMedia('/local/path/file.jpg')->toMediaCollection('collection');
$model->addMediaFromUrl('https://example.com/file.jpg')->toMediaCollection('collection');

Retrieving Media

$model->getFirstMediaUrl('collection');           // URL of first file
$model->getFirstMediaUrl('collection', 'thumb');  // URL of a conversion
$model->getFirstMedia('collection');              // Media object
$model->getMedia('collection');                   // All media in collection

Defining Collections

public function registerMediaCollections(): void
{
    $this->addMediaCollection('avatar')->singleFile();
    $this->addMediaCollection('gallery')->acceptsMimeTypes(['image/jpeg', 'image/png']);
}

Defining Conversions

public function registerMediaConversions(?Media $media = null): void
{
    $this->addMediaConversion('thumb')->width(300)->height(300);
    $this->addMediaConversion('thumb')->width(300)->height(300)->nonQueued(); // Synchronous
}

Deleting Media

$model->clearMediaCollection('collection');  // Delete all media in collection
$media->delete();                            // Delete a specific media item

Conclusion

Spatie Media Library takes the repetitive, error-prone parts of file handling in
Laravel and replaces them with a clean, consistent API. One media table, one trait
on your model, and you get file uploads, image conversions, collections, and URL
retrieval for every model in your app.

Key takeaways:

  • Install with composer require spatie/laravel-medialibrary, publish migrations, and run storage:link
  • Add the HasMedia interface and InteractsWithMedia trait to any model that needs files — no extra database columns required
  • Use addMediaFromRequest()->toMediaCollection() to attach uploaded files to a model
  • Define collections in registerMediaCollections() — use singleFile() for avatars and profile images
  • Define conversions in registerMediaConversions() to auto-generate thumbnails and resized versions
  • Always use ->with('media') when loading lists of models to avoid N+1 queries
  • Always validate file uploads before passing them to the media library
  • Run php artisan queue:work during development so image conversions are processed

Once you're comfortable with the basics, the package has a lot more to explore —
S3 and cloud storage, responsive images, custom path generators, and the paid
Media Library Pro for drag-and-drop upload components. But the fundamentals covered
here will get you through the vast majority of real-world use cases.