How to Upload Images to S3 and use CloudFront as CDN in NextJS

Mrinal Prakash
10 min readNov 18, 2024

--

Learn how to upload images to S3 and serve them via CloudFront in Next.js. Step-by-step guide to set up S3, CloudFront, and custom domains.

Hi there! This is Mrinal Prakash, and today I’m going to walk you through everything you need to know about uploading images to S3 and using CloudFront as a CDN in a Next.js application.

In this tutorial, we’ll cover creating an S3 bucket, setting up CloudFront for faster content delivery, programmatically uploading images using the AWS SDK, and configuring a custom domain for your CDN.

This guide is perfect for developers looking to optimize image delivery, improve performance, and scale their Next.js applications efficiently using AWS services. Let’s get started!

1. Create an S3 bucket to store the images

Step 1: Create an S3 Bucket

  • Navigate to the AWS S3 Console.
  • Click Create Bucket.
  • Name the bucket, e.g., masite.
  • Choose your preferred AWS region, e.g., ap-south-1.
  • Keep the bucket public access blocked for now. We’ll adjust permissions later.
  • Complete the setup by clicking Create Bucket.

Step 2: Configure the Bucket Policy

To allow uploads and public reads for images, add the following bucket policy:

{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": "*",
"Action": "s3:PutObject",
"Resource": "arn:aws:s3:::masite/*"
},
{
"Effect": "Allow",
"Principal": "*",
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::masite/*"
}
]
}
  • Go to the Permissions tab in the bucket settings.
  • Paste this policy under Bucket Policy and save.

Step 3: Uploading and Accessing an Image in S3

Once the necessary permissions are configured, we can upload an image to the S3 bucket and verify its accessibility. Follow these steps:

Uploading an Image to the S3 Bucket

  1. Navigate to your S3 bucket in the AWS Management Console.
  2. Under the Objects tab, locate and click the Upload button. This action will open the upload interface.
  3. In the upload interface:
  • Add Files: Click the Add files button to select the image you wish to upload from your local system.
  • Set Permissions: Ensure that the object permissions allow public access (if required for your use case) or follow your bucket’s policies.
  • Review and Confirm: Click Upload to start uploading the selected file.

Verifying the Uploaded Image

  • Once the upload is complete, you will see your file listed in the bucket under the Objects tab.
  • Click on the file name to view its properties and settings.

Testing Accessibility

  1. From the file details page, locate the Object URL. This is the public URL of your uploaded file.
  2. Copy the URL and paste it into a browser or an API client to confirm the file’s accessibility.
  • If the permissions are correctly configured, the image should load directly in the browser.
  • If it doesn’t load, double-check the bucket policy and object-level permissions.

This step ensures that the S3 bucket is correctly set up to store and serve images, making it ready for integration with CloudFront in the following steps.

2. Uploading Images to S3 Programmatically Using aws-sdk

Step 1: Install the AWS SDK

Add the AWS SDK to your project:

npm install aws-sdk

Step 2: Configure AWS Credentials

Create a .env.local file in your Next.js project and add your credentials:

AWS_ACCESS_KEY_ID=<YOUR_KEY_ID>
AWS_SECRET_ACCESS_KEY=<YOUR_ACCESS_KEY>
AWS_REGION=<YOUR_REGION>
BUCKET_NAME=<YOUR_BUCKET_NAME>

Step 3: Upload File Script

Create a file upload.js with the following code:

const AWS = require('aws-sdk');
const fs = require('fs');

const s3 = new AWS.S3({
accessKeyId: process.env.AWS_ACCESS_KEY_ID,
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,
region: process.env.AWS_REGION
});

const uploadFile = (bucketName, fileName) => {
const fileContent = fs.readFileSync(fileName);
const params = {
Bucket: bucketName,
Key: fileName,
Body: fileContent,
ContentType: 'image/png' // Update as per your file type
};

s3.upload(params, (err, data) => {
if (err) {
console.error('Error uploading file:', err);
} else {
console.log(`File uploaded successfully at ${data.Location}`);
}
});
};

// Usage: node upload.js masite pic.png
const bucketName = process.argv[2];
const fileName = process.argv[3];

uploadFile(bucketName, fileName);

Run the script to upload your image:

node upload.js masite pic.png

Alternatively, you can use the AWS CLI:

aws s3 cp pic.png s3://masite/pic.png

3. Creating a CloudFront Distribution and Attaching it to the S3 Bucket

  1. Go to the CloudFront Console in AWS.
  2. Click Create Distribution > Web.
  3. In Origin Settings:
  • Origin Domain Name: Select masite bucket from the list.
  • Origin Access Control Settings: Set to Public.

4. In Default Cache Behavior Settings:

  • Viewer Protocol Policy: Set to Redirect HTTP to HTTPS.
  • Finish creating the CloudFront distribution.

Expose the S3 Bucket on CDN Using CloudFront

  • Your CloudFront distribution will provide a Distribution Domain Name (e.g., d123abc4.cloudfront.net).
  • You can access any file in the bucket through CloudFront with a URL pattern like:
https://d123abc4.cloudfront.net/pic.png

Use a Custom Domain Name for CDN

To use a custom domain (e.g., cdn.emphay.com):

  1. Set up an SSL Certificate:
  • Go to AWS Certificate Manager (ACM) and request a certificate for cdn.emphay.com.
  • Validate the certificate by following AWS’s instructions. You can notice from the above screenshot that the certificate must be in US East (North Virginia) Region (us-east-1).

So in order to create certificate, click on Request Certificate, you will be redirected to the following page:

Now when you create the certificate, you will have to validate the certificate.

Configure CloudFront for the Custom Domain

  • In CloudFront > Your Distribution > Edit:
  • Add cdn.emphay.com as an Alternate Domain Name (CNAME).
  • Attach the certificate you created for cdn.emphay.com.

Update DNS

  • Go to your domain registrar’s DNS settings.
  • Add a CNAME record:
  • Name: cdn.emphay.com
  • Value: The CloudFront domain (e.g., d123abc4.cloudfront.net).

Now wait for 10 minutes, it would automatically show that the certificate is validated and issued.

Now we will move to the CloudFront Page and configure our certificates.

Now after you have configured, you will get to see the following page:

Now, we will try to add the CDN into my Domain Registrar settings:

  • Wait for the DNS changes to propagate. This may take some time (usually within 15–30 minutes, but can take up to 24–48 hours depending on the TTL settings).
  • You can check the DNS resolution by using tools like DNS Checker
  • You can also check by running the command
nslookup cdn.emphay.com

Once configured, your images will be accessible from https://cdn.emphay.com/pic.png.

Using a CDN in Next.js with S3 and CloudFront

Integrating Amazon S3 with CloudFront as a CDN in a Next.js application offers enhanced performance, scalability, and global distribution for your images. Here’s a detailed walkthrough of implementing this functionality.

Step 1: Setting Up AWS SDK in Your Next.js API

To interact with S3, we configure the AWS SDK and create an API endpoint for image uploads. This endpoint will handle the image data, process it, and upload it to S3.

import AWS from "aws-sdk";
import type { NextApiRequest, NextApiResponse } from "next";

AWS.config.update({
accessKeyId: process.env.AWS_ACCESS_KEY_ID,
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,
region: process.env.AWS_REGION,
});

const s3 = new AWS.S3();

export default async function uploadProfileImage(
req: NextApiRequest,
res: NextApiResponse
) {
if (req.method === "POST") {
try {
const { file, fileName } = req.body;

const buffer = Buffer.from(file, "base64");

const params = {
Bucket: process.env.BUCKET_NAME!,
Key: fileName,
Body: buffer,
ContentEncoding: "base64",
ContentType: "image/jpeg",
};

const data = await s3.upload(params).promise();
res.status(200).json({ url: data.Location });
} catch (error) {
console.error("Error uploading file:", error);
res.status(500).json({ error: "File upload failed" });
}
} else {
res.status(405).json({ error: "Method not allowed" });
}
}

Explanation:

  1. AWS SDK Configuration: The code initializes the AWS SDK by updating it with your AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, and AWS_REGION. This setup enables seamless interaction with the S3 service.
  2. API Route Definition: A Next.js API route (uploadProfileImage) is created to handle HTTP POST requests. This route processes the incoming image data and uploads it to S3.
  3. Processing the Image: The API expects file (in Base64 format) and fileName in the request body. It converts the Base64 string into a buffer, preparing it for upload.
  4. Uploading to S3: The file is uploaded using the s3.upload method with parameters like the S3 bucket name, file name, file data, encoding type, and MIME type. The uploaded file's URL is returned to the client.
  5. Error Handling: The code includes robust error handling. If any issue arises during the upload, it logs the error and returns a 500 status code with an error message.

Step 2: Creating the Upload Form in the Frontend

We use Ant Design for a user-friendly image upload interface. The form allows users to select, crop, and upload an image.

import React, { useState } from "react";
import { Form, Upload, message } from "antd";
import ImgCrop from "antd-img-crop";
import { RcFile, UploadFile } from "antd/lib/upload";

const UploadForm: React.FC = () => {
const [form] = Form.useForm();
const [tempLink, setTempLink] = useState<string>("");
const [s3Link, setS3Link] = useState<string>("");
const [cdnLink, setCdnLink] = useState<string>(""); // State for CDN link

const handleFileUpload = async (file: RcFile) => {
try {
// Generate and set the temporary link for preview
const tempUrl = URL.createObjectURL(file);
setTempLink(tempUrl);

// Convert file to base64 string
const reader = new FileReader();
reader.onloadend = async () => {
const base64 = reader.result?.toString().split(",")[1];

// Upload to the API, which uploads to S3
const response = await fetch("/api/uploadProfileImage", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
file: base64,
fileName: file.name,
}),
});

if (response.ok) {
const data = await response.json();
setS3Link(data.url); // Set the S3 URL in the state

// Generate the CDN link by replacing the S3 domain with the CDN domain
const s3Domain = new URL(data.url).hostname; // Extract the hostname from S3 URL
const cdnUrl = data.url.replace(s3Domain, "cdn.emphay.com"); // Replace S3 domain with CDN
setCdnLink(cdnUrl); // Set the CDN URL in the state

setFileList(prev => [
{ uid: "-1", url: data.url, name: "image" },
]);
message.success("File uploaded to S3 successfully");
} else {
throw new Error("Failed to upload file");
}
};
reader.readAsDataURL(file);
} catch (error) {
console.error("Error uploading file:", error);
message.error("File upload failed");
}
};

const [fileList, setFileList] = useState<UploadFile[]>([
{ uid: "-1", url: s3Link, name: "image" },
]);

const handleImageUpload = ({ fileList: newFileList }: { fileList: UploadFile[] }) => {
setFileList(newFileList);
};

return (
<Form form={form}>
<Form.Item>
<ImgCrop rotationSlider>
<Upload
customRequest={({ file }) => handleFileUpload(file as RcFile)}
listType="picture-card"
fileList={fileList}
onChange={handleImageUpload}
>
{fileList.length < 1 && "+ Upload"}
</Upload>
</ImgCrop>
</Form.Item>
<Form.Item>
<div>
<p>Temporary Link: <a href={tempLink} target="_blank" rel="noopener noreferrer">{tempLink}</a></p>
<p>S3 Bucket Link: <a href={s3Link} target="_blank" rel="noopener noreferrer">{s3Link}</a></p>
<p>CDN Link: <a href={cdnLink} target="_blank" rel="noopener noreferrer">{cdnLink}</a></p> {/* Display CDN link */}
</div>
</Form.Item>
</Form>
);
};

export default UploadForm;

Explanation:

  1. Ant Design and Image Cropping: The frontend uses antd for UI components and ImgCrop for a smooth cropping experience, enhancing user interaction during image uploads.
  2. Uploading the File: When a file is selected, it is converted to a Base64 string using FileReader. This string is sent to the backend API (/api/uploadProfileImage) for upload.
  3. Handling Responses: On successful upload, the response from the backend contains the S3 URL. The frontend generates a CDN URL by replacing the S3 domain with your CDN’s domain (cdn.emphay.com).
  4. Displaying Links: After the upload, three links are displayed: a temporary local preview, the S3 URL, and the CDN URL. These links allow easy access to the uploaded image.
  5. User Interface: The Upload component from antd provides a drag-and-drop interface for file selection. The state variables (tempLink, s3Link, cdnLink) ensure that the user sees real-time updates for the uploaded image links.

Now try to run your nextjs application using the following command:

npm run dev

and head over to the page which contains the UI for Uploading the image to S3.

Testing Image Upload

  • Select an image using the form and upload it.
  • Upon successful upload, the following links are displayed:
  • Temporary Link: A local preview of the uploaded file.
  • S3 Link: The URL of the file stored in the S3 bucket.
  • CDN Link: The CloudFront URL pointing to the same file for enhanced performance and delivery.

Key Points to Remember

  • Ensure your bucket policy and object permissions are correctly set to allow public or restricted access as per your requirements.
  • Replace the cdn.emphay.com domain with your CloudFront domain for generating CDN links.
  • Use environment variables to store sensitive data like AWS credentials and bucket names securely.

This setup not only optimizes the delivery of images using CloudFront but also integrates seamlessly with Next.js applications for dynamic and scalable file management.

Conclusion

In this guide, we’ve walked through the process of uploading images to Amazon S3 and delivering them using CloudFront as a CDN in a Next.js application. From configuring S3 buckets and CloudFront distributions to implementing robust backend and frontend code, we’ve covered all the essential steps to streamline image storage and distribution.

Leveraging S3 for secure storage and CloudFront for low-latency delivery enhances your application’s performance and scalability, ensuring an optimal user experience.

--

--

Mrinal Prakash
Mrinal Prakash

Written by Mrinal Prakash

💻 Passionate Coder | Frontend & Backend Developer 🌐 | Kubernetes & DevOps Enthusiast | 🕵️‍♂️ Ethical Hacker | 🎮 CTF Player

No responses yet