Muhammad Yasir Amin

Loading websiteโ€ฆ

More from Muhammad Yasir Amin

Build Along With Me

Practical walkthroughs on Laravel, React, APIs and deployment workflows.

Subscribe on YouTube

Laravel 13 Multiple File Upload Tutorial with Validation

๐Ÿ› ๏ธ Laravel Tutorials4 min read

Laravel 13 Multiple File Upload Tutorial with Validation

Muhammad Yasir Amin

Muhammad Yasir Amin

Senior Full Stack Engineerโ€ขNov 10, 2026
Laravel 13 Multiple File Upload Tutorial with Validation

To upload multiple files in Laravel 13, add the multiple attribute to a file input named files[], validate the array with files and files.* rules, then loop over $request->file('files') and call store() on each file. Save the stored paths in the database so you can find them later.

This tutorial targets Laravel 13 (PHP 8.3 to 8.5). The code was written for this article and not executed in a live project, so test it with small files first.

What do you need before you start?

  • A Laravel 13 app with a database connection.
  • PHP upload limits that fit your files: upload_max_filesize, post_max_size and max_file_uploads in php.ini.
  • For public downloads, run php artisan storage:link once.

Step 1: How do you create the table and model?

Code
php artisan make:model UploadedFile -m

Edit the migration to store the details you need:

PHP
Schema::create('uploaded_files', function (Blueprint $table) {
    $table->id();
    $table->string('original_name');
    $table->string('path');
    $table->string('mime_type')->nullable();
    $table->unsignedBigInteger('size');
    $table->timestamps();
});

Then allow mass assignment in the model and run php artisan migrate:

PHP
class UploadedFile extends Model
{
    protected $fillable = ['original_name', 'path', 'mime_type', 'size'];
}

Step 2: Which routes do you add?

CSS
use App\Http\Controllers\FileUploadController;
use Illuminate\Support\Facades\Route;

Route::get('/upload', [FileUploadController::class, 'create'])->name('upload.create');
Route::post('/upload', [FileUploadController::class, 'store'])->name('upload.store');

Step 3: How do you validate and store the files?

Create php artisan make:controller FileUploadController. Validate the array itself and each item inside it.

JavaScript
namespace App\Http\Controllers;

use App\Models\UploadedFile;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;

class FileUploadController extends Controller
{
    public function create()
    {
        return view('upload', ['files' => UploadedFile::latest()->get()]);
    }

    public function store(Request $request): RedirectResponse
    {
        $request->validate([
            'files' => ['required', 'array', 'min:1', 'max:5'],
            'files.*' => ['file', 'mimes:jpg,png,pdf,docx', 'max:2048'],
        ]);

        foreach ($request->file('files') as $file) {
            $path = $file->store('uploads', 'public');

            UploadedFile::create([
                'original_name' => $file->getClientOriginalName(),
                'path' => $path,
                'mime_type' => $file->getMimeType(),
                'size' => $file->getSize(),
            ]);
        }

        return back()->with('success', 'Files uploaded.');
    }
}

The max rule on a file is measured in kilobytes, so 2048 means 2 MB per file. store() generates a random file name, which avoids collisions and path tricks. Keep getClientOriginalName() only as display text; never use it as the stored file name.

Step 4: How do you build the upload form?

The form needs enctype="multipart/form-data" and an input named with square brackets.

Shell
<form action="{{ route('upload.store') }}" method="POST" enctype="multipart/form-data">
    @csrf
    <input type="file" name="files[]" multiple required>

    @error('files') <p>{{ $message }}</p> @enderror
    @error('files.*') <p>{{ $message }}</p> @enderror

    @if (session('success')) <p>{{ session('success') }}</p> @endif

    <button type="submit">Upload</button>
</form>

<ul>
    @foreach ($files as $file)
        <li><a href="{{ Storage::url($file->path) }}">{{ $file->original_name }}</a></li>
    @endforeach
</ul>

The @error('files.*') directive shows messages for any invalid item in the array.

How do you customise error messages for each file?

Pass a messages array as the second argument of validate():

JavaScript
$request->validate([
    'files.*' => ['file', 'mimes:jpg,png,pdf,docx', 'max:2048'],
], [
    'files.*.mimes' => 'Each file must be a JPG, PNG, PDF or DOCX.',
    'files.*.max' => 'Each file may not be larger than 2 MB.',
]);

How do you store files privately?

The public disk exposes files by URL. For private documents, store them on the default local disk with $file->store('private-uploads') and serve them through a controller that checks permissions, returning Storage::download($path). Never rely on an unguessable URL as the only protection.

Common mistakes

  • Forgetting the brackets. name="files" sends one file; name="files[]" sends an array.
  • Missing enctype. Without multipart/form-data the request has no files at all.
  • Validating only files. The array rule does not check each file. Add files.*.
  • Hitting PHP limits. If the validation passes nothing and $request->file('files') is empty, check post_max_size and max_file_uploads.
  • Trusting the client's extension. The mimes rule checks the detected content type, but still validate on the server and scan uploads if users are untrusted.
  • Forgetting storage:link. Public URLs return 404 until the symlink exists.

How do you verify it works?

  1. Open /upload, select two or three small files and submit.
  2. Check that the files exist under storage/app/public/uploads.
  3. Check the uploaded_files table has one row per file.
  4. Try a file with the wrong type and a sixth file, and confirm validation errors appear.

For an automated check, use UploadedFile::fake() from Illuminate\Http\UploadedFile with Storage::fake('public'), post an array of fake files and assert them with Storage::disk('public')->assertExists().

FAQ

How do I upload many large files?

Raise the PHP limits, or use chunked uploads or direct-to-S3 uploads with pre-signed URLs.

Can I delete a file later?

Yes: Storage::disk('public')->delete($file->path) and then delete the database row.

How do I upload with AJAX?

Build a FormData object, append each file under files[] and send it with fetch(), including the CSRF token header.

References

Article Index & Topic Tags
LaravelFile-UploadValidationStorage

YouTube Channel

Build Along With Me

Practical walkthroughs on Laravel, React, APIs and deployment workflows.

Muhammad Yasir Amin
Available for Contracts
Author & Solutions Architect

Senior Full Stack Engineer

Independent Senior Full Stack Engineer working on Laravel, React and Next.js applications, APIs and delivery workflows.

Laravel & PHP ApplicationsReact & Next.js InterfacesREST APIs & IntegrationsDatabase Queries & DebuggingDocker Development WorkflowsClear Scope & Handover
Discuss Your Next Step

What Does Your Application Need Next?

Share your current system, the problem you want to solve and what a useful result looks like. We can identify the right starting point and agree on a practical scope.

Technical Audit

Find the Next Useful Fix

Application Development

Scoped, Reviewable Milestones

Ongoing Support

Agreed Capacity and Priorities

Share your goals, current stack and requirements before the call.