The Problem with Traditional File Uploads
In many web applications, uploading files is a common requirement. Normally, we handle this by sending the file from our React frontend to our Laravel backend, which then processes it and moves it to AWS S3. While this works fine for small images or documents, it becomes a major bottleneck when dealing with large files like videos or zip archives.
When you upload large files through your PHP server, you run into several issues:
- Server Memory & Timeout Limits: You have to constantly adjust
upload_max_filesize,post_max_size, andmax_execution_timein yourphp.ini. - High Bandwidth Consumption: Your server has to download the file from the user and then upload it to S3, doubling the bandwidth usage.
- Poor User Experience: If the connection drops midway, the upload fails completely, and your server resources are wasted.
To solve this, we can bypass the PHP server entirely during the actual file transfer. By using AWS S3 pre-signed URLs, our React frontend can upload files directly to S3 securely.
How Do Pre-signed URLs Work?
Instead of making your S3 bucket completely public (which is a major security risk), you keep it private. When a user wants to upload a file, the process looks like this:
- The React frontend asks the Laravel backend for permission to upload a file.
- Laravel verifies the user's session and permission, then asks AWS S3 for a temporary, secure URL (a pre-signed URL) that is valid for only a few minutes.
- Laravel sends this URL back to React.
- React uploads the file directly to AWS S3 using this temporary URL.
Step 1: Setting Up Laravel to Generate Pre-signed URLs
First, make sure you have the Flysystem S3 driver installed in your Laravel project:
composer require league/flysystem-aws-s3-v3 "^3.0"Next, configure your AWS credentials in your .env file. Make sure your IAM user has the necessary permissions to write to your S3 bucket.
Now, let's create a controller method to generate the pre-signed URL. We will use Laravel's Storage facade to generate a temporary upload URL.
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
class FileUploadController extends Controller
{
public function getPresignedUrl(Request $request)
{
$request->validate([
'file_name' => 'required|string',
'file_type' => 'required|string',
]);
$fileName = $request->input('file_name');
$fileType = $request->input('file_type');
// Create a unique path for the file
$path = 'uploads/' . Str::uuid() . '-' . $fileName;
$client = Storage::disk('s3')->getClient();
$expiry = "+20 minutes";
$command = $client->getCommand('PutObject', [
'Bucket' => config('filesystems.disks.s3.bucket'),
'Key' => $path,
'ContentType' => $fileType,
]);
$presignedRequest = $client->createPresignedRequest($command, $expiry);
$url = (string) $presignedRequest->getUri();
return response()->json([
'upload_url' => $url,
'file_path' => $path,
]);
}
}Step 2: Configuring S3 Bucket CORS
Since your React frontend is running on a different domain (like localhost:3000 or myapp.com) than S3, AWS will block the direct upload unless you configure Cross-Origin Resource Sharing (CORS) on your S3 bucket.
Go to your AWS Console, open your S3 bucket, navigate to the Permissions tab, scroll down to CORS configuration, and add the following JSON:
[
{
"AllowedHeaders": ["*"],
"AllowedMethods": ["PUT"],
"AllowedOrigins": ["http://localhost:3000", "https://yourdomain.com"],
"ExposeHeaders": []
}
]Step 3: Uploading Files from React
Now, let's write the React component to handle the file selection, fetch the pre-signed URL from our Laravel API, and upload the file directly to S3 using a standard PUT request.
https://milinddaraniya.com/import React, { useState } from 'react';
function FileUploader() {
const [file, setFile] = useState(null);
const [uploading, setUploading] = useState(false);
const handleFileChange = (e) => {
setFile(e.target.files[0]);
};
const handleUpload = async () => {
if (!file) return;
setUploading(true);
try {
// 1. Get the pre-signed URL from Laravel
const response = await fetch('/api/get-presigned-url', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
},
body: JSON.stringify({
file_name: file.name,
file_type: file.type,
}),
});
const { upload_url, file_path } = await response.json();
// 2. Upload the file directly to S3 using PUT
const s3Response = await fetch(upload_url, {
method: 'PUT',
body: file,
headers: {
'Content-Type': file.type,
},
});
if (s3Response.ok) {
alert('Upload successful! File saved at: ' + file_path);
// Here, you can save the file_path to your MySQL database via another API call
} else {
alert('Upload failed.');
}
} catch (error) {
console.error('Error uploading file:', error);
} finally {
setUploading(false);
}
};
return (
<div>
<input type="file" onChange={handleFileChange} />
<button onClick={handleUpload} disabled={uploading}>
{uploading ? 'Uploading...' : 'Upload'}
</button>
</div>
);
}Wrapping Up
By shifting the upload process directly to AWS S3, you free up your Laravel server from handling heavy payloads. This improves application response times, reduces server bandwidth costs, and makes your app highly scalable. It is a simple architectural change that makes a huge difference in production environments.
Common Questions
Is it safe to upload files directly to S3 from the frontend?
Yes, because we use pre-signed URLs. The URL is generated securely by your Laravel backend after verifying user authorization, and it is configured to expire after a short time (e.g., 20 minutes).
Do I need to change my PHP.ini upload limits for this?
No. Since the file is sent directly from the browser to AWS S3, it never hits your PHP server. You can leave your PHP upload limits at their default settings.
Why do I get a CORS error when uploading from React?
This happens if your S3 bucket is not configured to accept requests from your frontend domain. You need to update the CORS settings in your AWS S3 bucket permissions to allow PUT requests from your frontend URL.