Efficient file uploads are essential for modern web applications, enhancing user experience and functionality. In this tutorial, we'll build a robust Laravel file upload API with Vue.js integration, focusing on progress tracking and efficient handling using modern best practices.

By leveraging Vue.js for the front end and Laravel's robust back end, we will create a seamless file upload system complete with a progress bar and comprehensive validation.

Prerequisites

To follow along with this tutorial, ensure you have:

  • Laravel 10.x or 11.x installed
  • Vue.js 3.x
  • Node.js 18.x or higher
  • Composer
  • Basic knowledge of Laravel and Vue.js

Setting up the laravel project

Create a new Laravel project using Composer:

composer create-project laravel/laravel file-upload-demo
cd file-upload-demo

Install the required dependencies:

composer install
npm install

For compiling front-end assets with Vite, run:

npm run dev

Set up your environment:

cp .env.example .env
php artisan key:generate
php artisan storage:link

Implementing a secure file upload API

Create a new controller for handling file uploads:

php artisan make:controller API/FileUploadController

Implement secure file handling in app/Http/Controllers/API/FileUploadController.php:

<?php

namespace App\Http\Controllers\API;

use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Storage;

class FileUploadController extends Controller
{
    public function upload(Request $request)
    {
        try {
            $validated = $request->validate([
                'file' => [
                    'required',
                    'file',
                    'max:10240',
                    'mimes:jpeg,png,pdf',
                    'mimetypes:image/jpeg,image/png,application/pdf'
                ]
            ]);

            // For demonstration, files are stored locally.
            // For production, consider using cloud storage such as Amazon S3:
            // $path = Storage::disk('s3')->put('uploads', $request->file('file'));
            $path = Storage::disk('public')->put('uploads', $request->file('file'));

            return response()->json([
                'success' => true,
                'message' => 'File uploaded successfully',
                'path'    => $path,
                'url'     => Storage::disk('public')->url($path)
            ]);
        } catch (\Exception $e) {
            return response()->json([
                'success' => false,
                'message' => 'Upload failed: ' . $e->getMessage()
            ], 500);
        }
    }
}

Add the route with rate limiting in routes/api.php:

Route::middleware(['throttle:uploads'])->group(function () {
    Route::post('/upload', [App\Http\Controllers\API\FileUploadController::class, 'upload']);
});

Configure rate limiting in app/Providers/RouteServiceProvider.php:

use Illuminate\Cache\RateLimiting\Limit;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\RateLimiter;

RateLimiter::for('uploads', function (Request $request) {
    return Limit::perMinute(60)->by(optional($request->user())->id ?: $request->ip());
});

Integrating vue.js for front-end file management

Create a new Vue component using the Composition API.

Create resources/js/components/FileUpload.vue:

<script setup>
import { ref } from 'vue'
import axios from 'axios'

const file = ref(null)
const progress = ref(0)
const message = ref('')
const uploading = ref(false)

const selectFile = (event) => {
  file.value = event.target.files[0]
  message.value = ''
}

const uploadFile = async () => {
  if (!file.value) return

  uploading.value = true
  const formData = new FormData()
  formData.append('file', file.value)

  try {
    const response = await axios.post('/api/upload', formData, {
      headers: { 'Content-Type': 'multipart/form-data' },
      onUploadProgress: (event) => {
        progress.value = Math.round((event.loaded * 100) / event.total)
      },
    })

    message.value = response.data.message || 'File uploaded successfully'
    file.value = null
  } catch (error) {
    message.value = `Upload failed: ${error.response?.data?.message || error.message}`
  } finally {
    uploading.value = false
    progress.value = 0
  }
}
</script>

<template>
  <div class="upload-container">
    <input
      type="file"
      @change="selectFile"
      :disabled="uploading"
      accept="image/jpeg,image/png,application/pdf"
    />

    <button @click="uploadFile" :disabled="!file || uploading" class="upload-button">
      {{ uploading ? 'Uploading...' : 'Upload' }}
    </button>

    <div v-if="progress > 0" class="progress-container">
      <div class="progress-bar" :style="{ width: `${progress}%` }">%</div>
    </div>

    <div v-if="message" :class="['message', { error: message.includes('failed') }]">
      {{ message }}
    </div>
  </div>
</template>

<style scoped>
.upload-container {
  max-width: 500px;
  margin: 20px auto;
  padding: 20px;
}

.progress-container {
  margin-top: 20px;
  background: #f0f0f0;
  border-radius: 4px;
  overflow: hidden;
}

.progress-bar {
  background: #4caf50;
  color: white;
  text-align: center;
  padding: 4px;
  transition: width 0.3s ease;
}

.message {
  margin-top: 10px;
  padding: 10px;
  border-radius: 4px;
}

.message.error {
  background: #ffebee;
  color: #c62828;
}

.upload-button {
  margin-top: 10px;
  padding: 8px 16px;
  background: #2196f3;
  color: white;
  border: none;
  border-radius: 4px;
  cursor: pointer;
}

.upload-button:disabled {
  background: #ccc;
  cursor: not-allowed;
}
</style>

Best practices for handling large file uploads

For production environments, implement these configurations:

  1. Update PHP settings in php.ini:
upload_max_filesize = 20M
post_max_size = 25M
max_execution_time = 300
  1. Configure Nginx (if using):
client_max_body_size 25M;
proxy_read_timeout 300;
proxy_connect_timeout 300;
proxy_send_timeout 300;
  1. Implement chunked uploads for large files using a package like laravel-chunk-upload.

Error handling and validation

Add a custom request class for validation:

php artisan make:request FileUploadRequest

Implement validation rules in app/Http/Requests/FileUploadRequest.php:

<?php

namespace App\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;

class FileUploadRequest extends FormRequest
{
    public function authorize()
    {
        return true;
    }

    public function rules()
    {
        return [
            'file' => [
                'required',
                'file',
                'max:10240',
                'mimes:jpeg,png,pdf',
                'mimetypes:image/jpeg,image/png,application/pdf'
            ]
        ];
    }

    public function messages()
    {
        return [
            'file.max' => 'The file size must not exceed 10MB.',
            'file.mimes' => 'The file must be a JPEG, PNG, or PDF.',
        ];
    }
}

Conclusion

We have built a robust file upload system using Laravel and Vue.js, implementing modern best practices for security, validation, and user experience. This solution features progress tracking, comprehensive error handling, and rate limiting to prevent abuse.

For more advanced features such as resumable uploads and cross-browser compatibility, consider using Uppy, an open-source file uploader that works seamlessly with Laravel.