How to 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.
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)
- 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": []
}
]
Add this bucket policy:
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "PublicReadAccess",
"Effect": "Allow",
"Principal": "*",
"Action": "s3:PutObject",
"Resource": "arn:aws:s3:::{{bucket-name}}/*"
}
]
}
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 @aws-sdk/s3-request-presigner
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";
// ✅ Initialize S3 client
const s3 = new S3Client({
region: process.env.AWS_REGION!,
credentials: {
accessKeyId: process.env.AWS_ACCESS_KEY_ID!,
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!,
},
});
// ✅ GET request handler
export async function GET(req: Request): Promise<Response> {
try {
const { searchParams } = new URL(req.url);
const fileName = searchParams.get("file");
const fileType = searchParams.get("type");
if (!fileName || !fileType) {
return new Response(
JSON.stringify({ error: "Missing file name or type" }),
{ status: 400, headers: { "Content-Type": "application/json" } }
);
}
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" },
status: 200,
});
} catch (error) {
console.error("Error generating presigned URL:", error);
return new Response(
JSON.stringify({ error: "Failed to generate URL" }),
{ status: 500, 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, ChangeEvent } from "react";
import { FaGithub, FaLinkedin, FaTwitter, FaYoutube } from "react-icons/fa"; // npm install react-icons
export default function UploadPage() {
const [file, setFile] = useState<File | null>(null);
const [loading, setLoading] = useState(false);
const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
setFile(e.target.files?.[0] ?? null);
};
const uploadFile = async () => {
if (!file) return alert("Select a file first!");
setLoading(true);
try {
const res = await fetch(
`/api/upload-url?file=${encodeURIComponent(file.name)}&type=${encodeURIComponent(file.type)}`
);
if (!res.ok) throw new Error("Could not get presigned URL");
const { url } = (await res.json()) as { url: string };
const upload = await fetch(url, { method: "PUT", body: file, headers: { "Content-Type": file.type } });
alert(upload.ok ? "✅ Uploaded!" : "❌ Upload failed");
setFile(null);
} catch (err) {
console.error(err);
alert("❌ Error uploading file");
} finally {
setLoading(false);
}
};
return (
<div className="min-h-screen bg-gray-900 text-gray-100 flex flex-col items-center p-6 font-sans pt-25">
<div className="w-full max-w-md mx-auto bg-gradient-to-br from-gray-900 to-gray-800 p-8 rounded-3xl shadow-2xl border border-gray-700 hover:scale-105 transform transition-all duration-300">
<h2 className="text-3xl font-bold mb-6 text-center text-white tracking-wide">
Upload File to S3
</h2>
<label
htmlFor="fileInput"
className="flex flex-col items-center justify-center w-full h-40 border-2 border-dashed border-gray-600 rounded-xl cursor-pointer hover:border-blue-500 hover:bg-gray-700 transition-colors"
>
<p className="text-gray-400 mb-2">Click or drag file here</p>
{file && (
<span className="text-sm text-blue-400 font-medium">{file.name}</span>
)}
<input
id="fileInput"
type="file"
onChange={handleChange}
className="hidden"
/>
</label>
<button
onClick={uploadFile}
disabled={loading || !file}
className="w-full mt-6 bg-blue-600 hover:bg-blue-700 disabled:opacity-50 text-white px-6 py-3 rounded-xl font-semibold text-lg transition-colors"
>
{loading ? "Uploading..." : "Upload"}
</button>
{!file && (
<p className="mt-4 text-gray-400 text-center text-sm">
Supported: Images, PDFs, Videos
</p>
)}
</div>
</div>
);
}
Step 6: Final Directory Structure
app/
├─ api/
│ └─ upload-url/
│ └─ route.js
├─ upload/
│ └─ page.jsx
.env.local
This structure is clean, modular, and ready for production.
Common Errors & Fixes
| Issue | Cause | Fix |
|---|---|---|
| CORS error | Missing or misconfigured CORS rules | Update S3 CORS config |
| 403 Forbidden | Incorrect AWS credentials or bucket | Check .env |
| Signature mismatch | Region mismatch | Verify S3Client region |
| Slow upload | Large file uploads | Use multipart upload or S3 Transfer Acceleration |
Frequently Asked Questions (FAQs)
1. How do I upload a file from Next.js to AWS S3?
You can upload files from Next.js to AWS S3 using Presigned URLs. First, create a backend API route that generates a signed URL using the AWS SDK v3, then upload the file directly from your frontend using a simple fetch() PUT request. This keeps uploads secure and fast without storing files on your server.
2. What is a Presigned URL in AWS S3?
A Presigned URL is a temporary, secure link that allows users to upload or download files directly from an S3 bucket without exposing AWS credentials. It’s commonly used in Next.js and Node.js applications for safe file uploads.
3. Why should I use Presigned URLs instead of uploading via my server?
Presigned URLs let your users upload files directly to S3, bypassing your server. This reduces bandwidth load, improves performance, and enhances security since credentials never reach the client.
4. How do I generate a Presigned URL using AWS SDK v3 in Next.js?
Use the getSignedUrl function from the @aws-sdk/s3-request-presigner package. In your Next.js API route, configure an S3Client with credentials and call getSignedUrl() on a PutObjectCommand to generate the URL.
5. Why am I getting a CORS error when uploading to S3 from Next.js?
CORS errors usually mean your S3 bucket’s CORS configuration is missing or misconfigured. Ensure you’ve allowed the correct methods (GET, PUT, POST) and your frontend domain in the bucket’s CORS rules.
6. Can I use this method for images, PDFs, or videos?
Yes! The same Presigned URL upload method works for any file type — images, PDFs, videos, or documents. Just make sure you set the correct Content-Type when generating the Presigned URL and uploading the file.
7. Is AWS SDK v3 better than v2 for Next.js projects?
Yes. The AWS SDK v3 is modular, lightweight, and optimized for modern JavaScript frameworks like Next.js. It lets you import only the packages you need, improving performance and bundle size.
8. How do I keep my AWS keys safe in Next.js?
Store them in your .env.local file and never hardcode them. Next.js automatically loads environment variables from .env.local on the server side only, keeping them secure.
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: https://github.com/ahadali0500/nextjs-s3-presigned-upload Watch the full YouTube tutorial here: https://youtu.be/your-video-link
Follow me for more DevOps + Next.js guides!