Upload Files from Next.js to AWS S3 Using Presigned URLs

In modern web apps, users often need to upload images, documents, or videos. Amazon S3 is one of the best cloud storage options — but directly uploading files from a frontend like Next.js can expose your AWS credentials.
In this tutorial, you’ll learn how to securely upload files from a Next.js app to AWS S3 using presigned URLs, step by step.
Table of Contents
Step 1 – Create and Configure an S3 Bucket Step 2 – Add Environment Variables Step 3 – Install AWS SDK v3 Step 4 – Create API Route for Presigned URL Step 5 – Upload from Frontend Step 6 – Common Errors & Fixes Step 7 – Final Directory Structure Step 8 – Conclusion
🧠 Why Presigned URLs?
When you generate a presigned URL on the server, it gives the client temporary permission to upload files to S3 — without exposing your AWS secret keys. ✅ Secure ✅ Fast (direct upload to AWS, not via backend) ✅ Scalable for production
⚙️ Prerequisites
Before you begin: AWS account (with S3 access) Node.js & Next.js project setup Basic understanding of environment variables
🔧 Step 1: Create and Configure an S3 Bucket
Go to your AWS Console → S3 → Create Bucket
Choose a unique bucket name
Enable required region (e.g., ap-south-1)
In Permissions, uncheck “Block all public access” if you want public access for uploaded files (optional).
Add this CORS configuration (important):
[
{
"AllowedHeaders": ["*"],
"AllowedMethods": ["GET", "PUT"],
"AllowedOrigins": ["*"],
"ExposeHeaders": []
}
]
🔑 Step 2: Add Environment Variables
Create a .env.local file in your Next.js project and add: AWS_ACCESS_KEY_ID=your_access_key AWS_SECRET_ACCESS_KEY=your_secret_key AWS_REGION=ap-south-1 S3_BUCKET_NAME=your_bucket_name
Make sure not to commit these to GitHub (they’re secret).
🧩 Step 3: Install AWS SDK v3
Run this command:
npm install @aws-sdk/client-s3
🧠 Step 4: Create an API Route for Presigned URL
Inside your Next.js app, create the following file: /app/api/upload-url/route.js (for Next.js 13+ with App Router)
import { S3Client, PutObjectCommand } from "@aws-sdk/client-s3";
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
const s3 = new S3Client({
region: process.env.AWS_REGION,
credentials: {
accessKeyId: process.env.AWS_ACCESS_KEY_ID,
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,
},
});
export async function GET(req) {
const { searchParams } = new URL(req.url);
const fileName = searchParams.get("file");
const fileType = searchParams.get("type");
const command = new PutObjectCommand({
Bucket: process.env.S3_BUCKET_NAME,
Key: fileName,
ContentType: fileType,
});
const signedUrl = await getSignedUrl(s3, command, { expiresIn: 60 });
return new Response(JSON.stringify({ url: signedUrl }), {
headers: { "Content-Type": "application/json" },
});
}
👉 This route: Generates a signed URL valid for 60 seconds
Returns it to the client so the file can be uploaded securely
🧠 Step 5: Upload from Frontend
Now, create a simple upload component: /app/upload/page.jsx
"use client";
import { useState } from "react";
export default function UploadPage() {
const [file, setFile] = useState(null);
const [uploading, setUploading] = useState(false);
const handleUpload = async () => {
if (!file) return;
setUploading(true);
// 1. Request presigned URL from API
const res = await fetch(
`/api/upload-url?file=${file.name}&type=${file.type}`
);
const { url } = await res.json();
// 2. Upload file directly to S3
const upload = await fetch(url, {
method: "PUT",
body: file,
headers: { "Content-Type": file.type },
});
if (upload.ok) {
alert("✅ File uploaded successfully!");
} else {
alert("❌ Upload failed.");
}
setUploading(false);
};
return (
<div className="flex flex-col items-center mt-20">
<input
type="file"
onChange={(e) => setFile(e.target.files[0])}
className="mb-4"
/>
<button
onClick={handleUpload}
disabled={uploading}
className="bg-blue-600 text-white px-4 py-2 rounded"
>
{uploading ? "Uploading..." : "Upload"}
</button>
</div>
);
}
⚠️ Step 6: Common Errors & Fixes
Error Cause Fix 403 Forbidden Wrong IAM permissions Ensure bucket allows PutObject for your AWS user CORS error CORS policy missing in bucket Add correct CORS config (see Step 1) AccessDenied Wrong region or key Check .env.local values
🧾 Step 7: Final Directory Structure
app/
├─ api/
│ └─ upload-url/
│ └─ route.js
├─ upload/
│ └─ page.jsx
.env.local
✅ Conclusion
You’ve successfully learned how to upload files from Next.js to AWS S3 using presigned URLs. This approach is secure, efficient, and scalable for production use. If you found this helpful: ⭐ Check out the full source code on GitHub: github.com/ahadali0500/nextjs-s3-upload
🧠 Follow me for more DevOps + Next.js guides!