Upload Files to AWS S3 with Laravel 11 & Next.js Using Pre-signed URLs
Table of Contents Introduction Prerequisites Setting Up AWS S3 Bucket Configuring Laravel Backend Installing AWS SDK Setting Up Environment Variables Creating the Route and Controller Implementing Next.js Frontend Uploading Files Using Pre-signed URLs Creating the Upload Component Conclusion Introduction Uploading files directly to Amazon S3 using pre-signed URLs is a common pattern that allows clients to upload files without the files passing through your server. This method reduces server load and can improve upload performance. In this guide, we'll walk through setting up a Laravel backend to generate these pre-signed URLs and a Next.js frontend to upload files using them. Prerequisites Before we begin, ensure you have the following: An AWS account with access to S3. Laravel 11 installed. A Next.js project set up. Setting Up AWS S3 Bucket Create an S3 Bucket: Log in to your AWS Management Console. Navigate to the S3 service. Click on "Create bucket." Evervault - Docs Provide a unique name and select a region. Adjust other settings as needed and create the bucket. Evervault - Docs Set Bucket Permissions: To allow uploads using pre-signed URLs, ensure your bucket has the appropriate permissions. You can set a bucket policy that grants the necessary permissions. Configure CORS Policy: Set up a CORS policy to allow your frontend application to interact with the S3 bucket: json Copy Edit { "CORSRules": [ { "AllowedHeaders": ["*"], "AllowedMethods": ["PUT"], "AllowedOrigins": ["*"], "ExposeHeaders": [] } ] } Replace "*" in AllowedOrigins with your frontend's URL in a production environment for enhanced security. Configuring Laravel Backend a. Installing AWS SDK First, install the AWS SDK for PHP using Composer: bash Copy Edit composer require aws/aws-sdk-php b. Setting Up Environment Variables Add your AWS credentials and bucket information to your .env file: env Copy Edit AWS_ACCESS_KEY_ID=your_access_key_id AWS_SECRET_ACCESS_KEY=your_secret_access_key AWS_DEFAULT_REGION=your_region AWS_BUCKET=your_bucket_name Ensure these variables are correctly set to avoid authentication issues. c. Creating the Route and Controller Define a route in your routes/web.php or routes/api.php: php Copy Edit use Illuminate\Http\Request; use Illuminate\Support\Facades\Route; use Aws\S3\S3Client; Route::post('/get-s3-signed-url', function (Request $request) { $request->validate([ 'filename' => 'required|string|max:255', 'filetype' => 'required|string|in:video/mp4,video/quicktime,video/x-msvideo', ]); $s3 = new S3Client([ 'version' => 'latest', 'region' => env('AWS_DEFAULT_REGION'), 'credentials' => [ 'key' => env('AWS_ACCESS_KEY_ID'), 'secret' => env('AWS_SECRET_ACCESS_KEY'), ], ]); $key = 'uploads/videos/' . uniqid() . '_' . preg_replace('/[^a-zA-Z0-9_\-.]/', '_', $request->filename); $command = $s3->getCommand('PutObject', [ 'Bucket' => env('AWS_BUCKET'), 'Key' => $key, 'ContentType' => $request->filetype, 'ACL' => 'bucket-owner-full-control', ]); $signedUrl = $s3->createPresignedRequest($command, '+15 minutes')->getUri(); return response()->json([ 'signed_url' => (string)$signedUrl, 'public_url' => $s3->getObjectUrl(env('AWS_BUCKET'), $key), 'key' => $key, 'expires_at' => now()->addMinutes(15)->toIso8601String(), ]); }); Implementing Next.js Frontend a. Uploading Files Using Pre-signed URLs Create a function to handle file uploads using the pre-signed URL: javascript Copy Edit import axios from 'axios'; export const uploadVideoToS3 = async (file, onProgress) => { const timestamp = Date.now(); const extension = file.name.split('.').pop(); const baseName = file.name.replace(/.[^/.]+$/, ''); const safeName = ${timestamp}_${baseName.replace(/[^a-zA-Z0-9-_]/g, '')}.${extension}; const response = await axios.post( ${process.env.NEXT_PUBLIC_API_BASE_URL}/api/get-s3-signed-url, { filename: safeName, filetype: file.type, }, { headers: { 'Content-Type': 'application/json', }, } ); const { signed_url, public_url } = response.data; await axios.put(signed_url, file, { headers: { 'Content-Type': file.type, 'x-amz-server-side-encryption': 'AES256', ' ::contentReference[oaicite:37]{index=37}

Table of Contents
Introduction
Prerequisites
Setting Up AWS S3 Bucket
Configuring Laravel Backend
Installing AWS SDK
Setting Up Environment Variables
Creating the Route and Controller
Implementing Next.js Frontend
Uploading Files Using Pre-signed URLs
Creating the Upload Component
Conclusion
Introduction
Uploading files directly to Amazon S3 using pre-signed URLs is a common pattern that allows clients to upload files without the files passing through your server. This method reduces server load and can improve upload performance. In this guide, we'll walk through setting up a Laravel backend to generate these pre-signed URLs and a Next.js frontend to upload files using them.Prerequisites
Before we begin, ensure you have the following:
An AWS account with access to S3.
Laravel 11 installed.
A Next.js project set up.
- Setting Up AWS S3 Bucket Create an S3 Bucket:
Log in to your AWS Management Console.
Navigate to the S3 service.
Click on "Create bucket."
Evervault - Docs
Provide a unique name and select a region.
Adjust other settings as needed and create the bucket.
Evervault - Docs
Set Bucket Permissions:
To allow uploads using pre-signed URLs, ensure your bucket has the appropriate permissions. You can set a bucket policy that grants the necessary permissions.
Configure CORS Policy:
Set up a CORS policy to allow your frontend application to interact with the S3 bucket:
json
Copy
Edit
{
"CORSRules": [
{
"AllowedHeaders": ["*"],
"AllowedMethods": ["PUT"],
"AllowedOrigins": ["*"],
"ExposeHeaders": []
}
]
}
Replace "*" in AllowedOrigins with your frontend's URL in a production environment for enhanced security.
- Configuring Laravel Backend a. Installing AWS SDK First, install the AWS SDK for PHP using Composer:
bash
Copy
Edit
composer require aws/aws-sdk-php
b. Setting Up Environment Variables
Add your AWS credentials and bucket information to your .env file:
env
Copy
Edit
AWS_ACCESS_KEY_ID=your_access_key_id
AWS_SECRET_ACCESS_KEY=your_secret_access_key
AWS_DEFAULT_REGION=your_region
AWS_BUCKET=your_bucket_name
Ensure these variables are correctly set to avoid authentication issues.
c. Creating the Route and Controller
Define a route in your routes/web.php or routes/api.php:
php
Copy
Edit
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;
use Aws\S3\S3Client;
Route::post('/get-s3-signed-url', function (Request $request) {
$request->validate([
'filename' => 'required|string|max:255',
'filetype' => 'required|string|in:video/mp4,video/quicktime,video/x-msvideo',
]);
$s3 = new S3Client([
'version' => 'latest',
'region' => env('AWS_DEFAULT_REGION'),
'credentials' => [
'key' => env('AWS_ACCESS_KEY_ID'),
'secret' => env('AWS_SECRET_ACCESS_KEY'),
],
]);
$key = 'uploads/videos/' . uniqid() . '_' . preg_replace('/[^a-zA-Z0-9_\-.]/', '_', $request->filename);
$command = $s3->getCommand('PutObject', [
'Bucket' => env('AWS_BUCKET'),
'Key' => $key,
'ContentType' => $request->filetype,
'ACL' => 'bucket-owner-full-control',
]);
$signedUrl = $s3->createPresignedRequest($command, '+15 minutes')->getUri();
return response()->json([
'signed_url' => (string)$signedUrl,
'public_url' => $s3->getObjectUrl(env('AWS_BUCKET'), $key),
'key' => $key,
'expires_at' => now()->addMinutes(15)->toIso8601String(),
]);
});
- Implementing Next.js Frontend a. Uploading Files Using Pre-signed URLs Create a function to handle file uploads using the pre-signed URL:
javascript
Copy
Edit
import axios from 'axios';
export const uploadVideoToS3 = async (file, onProgress) => {
const timestamp = Date.now();
const extension = file.name.split('.').pop();
const baseName = file.name.replace(/.[^/.]+$/, '');
const safeName = ${timestamp}_${baseName.replace(/[^a-zA-Z0-9-_]/g, '')}.${extension}
;
const response = await axios.post(
${process.env.NEXT_PUBLIC_API_BASE_URL}/api/get-s3-signed-url
,
{
filename: safeName,
filetype: file.type,
},
{
headers: {
'Content-Type': 'application/json',
},
}
);
const { signed_url, public_url } = response.data;
await axios.put(signed_url, file, {
headers: {
'Content-Type': file.type,
'x-amz-server-side-encryption': 'AES256',
'
::contentReference[oaicite:37]{index=37}