Programming Beginner 9 min

How to Upload and Store Files in Laravel

Laravel's filesystem abstraction makes file uploads straightforward and swappable. Whether you're storing files locally, in the public disk, or on Amazon S3, the same store() API works across all drivers.

This guide walks through the full cycle: the HTML form, validation, storing the file, creating the public symlink, and displaying the URL. Then it covers moving to S3 when your local disk is no longer enough.

Step-by-step

  1. 1

    Write the upload form

    The form must have enctype="multipart/form-data" — without it, the file binary is never sent to the server. Use the @csrf directive and set method="POST".

    html
    <form action="{{ route('upload.store') }}" method="POST" enctype="multipart/form-data">
        @csrf
        <input type="file" name="image" accept="image/*">
        @error('image')
            <p class="error">{{ $message }}</p>
        @enderror
        <button type="submit">Upload</button>
    </form>
  2. 2

    Validate the uploaded file

    Always validate before storing. Use the file rule to confirm something was uploaded, mimes to restrict by extension, and max to cap the file size in kilobytes. Laravel checks both the file extension and the MIME type reported by the browser.

    php
    <?php
    
    namespace App\Http\Controllers;
    
    use Illuminate\Http\Request;
    
    class UploadController extends Controller
    {
        public function store(Request $request)
        {
            $request->validate([
                'image' => [
                    'required',
                    'file',
                    'mimes:jpg,jpeg,png,webp,pdf',
                    'max:2048', // 2 MB
                ],
            ]);
    
            // proceed to store...
        }
    }
  3. 3

    Store the file

    Call $request->file('image')->store('uploads', 'public'). Laravel generates a unique filename automatically (UUID-based), moves the file to storage/app/public/uploads/, and returns the relative path like uploads/abc123.jpg. Save that path to the database.

    If you want to control the filename yourself, use storeAs() instead.

    php
    // Auto-generated unique filename
    $path = $request->file('image')->store('uploads', 'public');
    // returns: "uploads/3f7a...uuid...jpg"
    
    // Custom filename
    $filename = time() . '_' . $request->file('image')->getClientOriginalName();
    $path = $request->file('image')->storeAs('uploads', $filename, 'public');
    
    // Save the path to the database
    $post = Post::create([
        'title' => $request->title,
        'image_path' => $path,
    ]);
  4. 4

    Create the public symlink

    The public disk stores files in storage/app/public/, which is not web-accessible by default. Run php artisan storage:link once to create a symlink from public/storagestorage/app/public. Do this after deployment on every server.

    bash
    php artisan storage:link
    # Creates: public/storage -> storage/app/public
  5. 5

    Display the stored file

    Use the Storage facade's url() method to generate the public URL from the stored path. It works for both the public disk (returning a local URL) and S3 (returning the CDN or S3 URL).

    html
    {{-- In a Blade template --}}
    <img src="{{ Storage::disk('public')->url($post->image_path) }}" alt="Uploaded image">
    
    {{-- Or use the global helper --}}
    <img src="{{ asset('storage/' . $post->image_path) }}" alt="Uploaded image">
    
    {{-- In a controller or model --}}
    use Illuminate\Support\Facades\Storage;
    
    $url = Storage::disk('public')->url($post->image_path);
    // returns: https://yourdomain.com/storage/uploads/abc123.jpg
  6. 6

    Understand filesystem disks

    Laravel's config/filesystems.php defines named disks. The three you'll use most often are:

    • local — stores files in storage/app/, not publicly accessible (good for private documents)
    • public — stores in storage/app/public/, symlinked to public/storage (good for user avatars, images)
    • s3 — stores on Amazon S3 (production-grade, scales infinitely)

    You switch disks by changing the second argument to store() or calling Storage::disk('s3'). Your controller code doesn't change.

  7. 7

    Configure the S3 disk

    Install the Flysystem S3 adapter, then add your AWS credentials to .env. The S3 disk configuration is already in config/filesystems.php — you just need to fill the env variables.

    bash
    composer require league/flysystem-aws-s3-v3 "^3.0" --with-all-dependencies
  8. 8

    Add S3 credentials to .env

    Add the following to your .env file. The AWS_URL is optional — use it if you have a CloudFront distribution in front of your bucket so Storage::url() returns your CDN URL instead of the raw S3 URL.

    bash
    # .env
    AWS_ACCESS_KEY_ID=AKIA...
    AWS_SECRET_ACCESS_KEY=your-secret-key
    AWS_DEFAULT_REGION=us-east-1
    AWS_BUCKET=your-bucket-name
    AWS_URL=https://cdn.yourdomain.com  # optional, for CloudFront
    
    FILESYSTEM_DISK=s3  # make S3 the default disk
  9. 9

    Upload to S3 and generate a URL

    Now swap the disk name in your store() call. Everything else in the controller stays the same. Files land in your S3 bucket under the uploads/ prefix.

    php
    // Store on S3
    $path = $request->file('image')->store('uploads', 's3');
    
    // Make the file publicly readable when uploading
    $path = $request->file('image')->storePublicly('uploads', 's3');
    
    // Generate the public URL
    $url = Storage::disk('s3')->url($path);
    // returns: https://your-bucket.s3.amazonaws.com/uploads/abc123.jpg

Tips & gotchas

  • Never trust the client-supplied filename — always let Laravel generate one, or sanitize it yourself before calling `storeAs()`. Original filenames can contain path traversal characters.
  • Validate by MIME type server-side using the `mimetypes` rule (e.g., `mimetypes:image/jpeg,image/png`), not just by extension — extensions are trivially spoofed.
  • For private files (contracts, invoices), store on the `local` disk and serve them through a controller that checks authorization — never use the `public` disk for sensitive documents.
  • Delete old files when replacing them: `Storage::disk('public')->delete($post->image_path)` before saving the new path — otherwise your storage fills up silently.
  • On S3, use presigned URLs (`Storage::temporaryUrl($path, now()->addMinutes(5))`) for private files instead of making the entire bucket public.

Wrapping up

You can now handle file uploads end-to-end in Laravel — from the validated form submission to storing on the local public disk or S3, and generating the correct URL for display. The natural next step is to add image processing (resize, crop, convert to WebP) using spatie/image or intervention/image before storing the file.

#Laravel #Files #S3
Back to all guides

Need Help With Your Project?

Book a free 30-minute consultation to discuss your technical challenges and explore solutions together.