From App to Cloud: How to Upload Images to AWS S3 from Your Web App with Node.js and Multer
I recently added another feature in my NodeJS app P_Blog to enhance the user experience. Now, users can directly upload images to an AWS S3 bucket from the app and copy the image link to use within their blog posts. This is especially convenient because P_Blog supports Markdown, making it easy for users to embed images by simply pasting the link.
Before implementing this feature, users had to rely on third-party platforms to host their images, which added unnecessary steps to the process. By allowing direct uploads to S3, the image management process becomes seamless, reducing friction and making content creation faster and more enjoyable for users. Now, they can focus more on writing and less on managing image links or worrying about image hosting.
In this blog post, I want to walk you through the process of uploading images to an AWS S3 bucket directly from your web app. I’ll explain how I implemented this feature using Node.js, Express.js, and Multer to handle file uploads, and how I integrated the AWS SDK to securely store images in the cloud. By the end of this guide, you’ll be able to seamlessly add image upload functionality to your own app, improving the user experience.
Setting Up AWS S3
- Login in to the AWS Management Console
- Navigate to
S3
and click onCreate bucket
- Give the bucket a unique name and make sure to uncheck
Block all public access
because we will make our bucket public so that images within the bucket become public - Now, we need to add a
Bucket policy
that allows anyone to view images located inside the bucket. For that go to thePermissions
tab and edit the bucket policy. You can usePolicy generator
to get the bucket policy and copy and paste the policy
{
"Version": "2012-10-17",
"Id": "Policy1725365190218",
"Statement": [
{
"Sid": "Stmt1725365183685",
"Effect": "Allow",
"Principal": "*",
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::your-bucket-name/*"
}
]
}
Creating an IAM User and Policy for S3
In this process, we’ll create an IAM user that will give programmatic access to AWS resources via an access key and a secret access key. These credentials allow our application to interact with AWS services, such as S3. We’ll also attach a policy to this user, specifically allowing them to put objects into the S3 bucket—meaning they can upload images. By restricting permissions in this way, we ensure that the IAM user has only the necessary access to upload files, enhancing the security of our app.
Creating Policy for S3
- Go to the
IAM Dashboard
- Go to the
Policies
section and click onCreate policy
and you will seePolicy editor
- In the
Select a service
section chooseS3
and search and check forPutObject
inActions allowed
section - In the
Resources
section chooseSpecific
and click onAdd ARNs
Specify ARN box will open - In the Speify ARN box enter your bucket name, Resource ARN check
Any object name
forResource object name
lastly click onAdd ARNs
button - Now we are in
Review and create
section. In this section give a meaningful name for this policy and click onCreate policy
button
Creating an IAM User
- Go to the
Users
section ofIAM Dashboard
- Click on
Create user
and give a meaningful name and click onNext
- In the
Set permissions
section chooseAttach policies directly
and search and choose for the policy that we have created above. Lastly, click onNext
button - In the
Review and create
section just need to click onCreate user
button - Go the the newly created user dashboard, click on
Create access key
and you can chooseOther
inAccess key best practices & alternatives
section. Lastly, click onNext
button - In
Set description tag
tag you may write description of this access key but it is optional and click onCreate access key
- This is the final and important part, where we will get the
Access key
andSecret access key
. You will see both keys in thisRetrieve access key
section. Make sure you have saved those keys somewhere safe.
Backend Setup
Multer setup to handle file uploads
First, we will set up a basic multer configuration for handling file uploads. This configuration is crucial for managing file zise limits and filtering which types of files are allowed to be uploaded. Install multer
by running npm install multer
command in your terminal.
I have created a file named s3ImageUpload.js
inside the folder utils
and the following code goes in that file:
const multer = require("multer");
const s3Upload = multer({
limits: {
fileSize: 12000000,
},
fileFilter: (req, file, cb) => {
const allowedFileType = ["image/png", "image/jpg", "image/jpeg"];
if (!allowedFileType.includes(file.mimetype)) {
return cb(
new Error("Please select a valid image file (PNG, JPG, JPEG)"),
false
);
}
cb(null, true);
},
});
module.exports = s3Upload;
Here, we have imported multer
set the file file size limit of 12MB. The fileFilter
function allows us to control what types of files can be uploaded. This ensures that users cannot upload excessively large files and files that are not included in allowedFileType
. In case of that multer
will throw an error and our custom error handler will catch and send this error to the frontend.
const errorHandler = (error, req,res,next) =>{
const errorStatusCode = res.statusCode || 500;
let errorMessage = error.message || "Something went wrong!";
res.status(errorStatusCode).json({message: errorMessage, success: false});
}
module.exports = errorHandler
Integrating AWS SDK for Image Uploads
In this section, we will defines an Express.js route for uploading images to an AWS S3 bucket. It uses the AWS SDK, along with several utility modules for authentication and file uploads. I have created a file named s3UploadImageRoute.js
inside routes
folder. First, let's import the required utility modules and setup aws-sdk.
const s3UploadImageRoute = require("express").Router();
const { S3Client, PutObjectCommand } = require("@aws-sdk/client-s3");
const crypto = require("crypto");
const path = require("path");
const s3Upload = require("../utils/s3ImageUpload");
const {islogin} = require("../utils/loginHandeler");
const bucketURL = "https://pblog-images-bucket.s3.ap-southeast-2.amazonaws.com";
const client = new S3Client({
region: "ap-southeast-2",
credentials: {
accessKeyId: process.env.AWS_S3_ACCESS_KEY,
secretAccessKey: process.env.AWS_S3_SECRET_ACCESS_KEY,
},
});
In above code, first we have set up an Express Router
for handling routes related to S3 image uploads. The S3Client
is used to communicate with the S3 service, while PutObjectCommand
represents the command to upload an object (in this case, an image) to the S3 bucket.
crypto
: Used for generating unique filenames for the images by creating a random string (to avoid overwriting files with the same name).
path
: Used for extracting file extensions to properly name the uploaded image files.
s3Upload
: Imports a pre-configured Multer instance for handling the file upload (defined earlier).
islogin
: A custom utility function that checks if the user is logged in before allowing access to the image upload route. This ensures that only authenticated users can upload images.
bucketURL
: This constant stores the base URL of the S3 bucket where images will be uploaded. Once an image is successfully uploaded, you can append the image’s unique filename to this URL to create the full link for accessing the image.
client
: an S3Client
instance to interact with the S3 bucket configured with Region
, Credentials
where we have used previously created IAM user's accessKeyId
and secretAccessKey
stored in environment variables.
Now, let's define the POST
route for uploading an image to an S3 bucket
that processes the uploaded file, and send it to AWS S3.
s3UploadImageRoute.post("/", islogin, s3Upload.single("s3Image"), async (req, res) => {
if (!req.file) {
return res.status(400).json({
message: "No image selected",
success: false,
});
}
const fileExt = path.extname(req.file.originalname);
const fileName = `${crypto.randomBytes(16).toString("hex")}.${fileExt}`;
const input = {
Bucket: "pblog-images-bucket",
Key: fileName,
Body: req.file.buffer,
};
const command = new PutObjectCommand(input);
try {
const response = await client.send(command);
if (response.$metadata.httpStatusCode !== 200) {
throw new Error("Failed to upload image to s3 bucket");
}
res.json({ imageURL: `${bucketURL}/${fileName}`, success: true });
} catch (er) {
console.log(er);
res.status(500).json({ message: er.message, success: false });
}
});
module.exports = s3UploadImageRoute;
s3Upload.single("s3Image")
uses Multer to handle the file upload, expecting a single file with the field name s3Image
. If no file is provided, the server responds with a 400 Bad Request status and an error message stating, No image selected
. fileExt
extracts the file extension from the uploaded file (e.g., .jpg, .png) using path.extname()
. fileName
generates a unique filename by combining a random 16-byte hexadecimal string (via crypto.randomBytes()
) with the original file extension. This ensures that every file has a unique name, preventing conflicts if two users upload files with the same name. command
use PutObjectCommand
to create a command with the input
data. This command is responsible for telling AWS S3 to upload the file. client.send(command)
sends the upload command to AWS S3 which returns a response
object.
If the upload is successful, the server responds with a JSON object containing the URL
of the uploaded image (constructed by appending the fileName
to the bucketURL
) and a success flag (success: true
).
Lastly, we need to configure our s3UploadImageRoute
in app.js
file as following:
const s3UploadImageRoute = require("./routes/s3UploadImageRoute");
const errorHandler = require("./utils/errorHandler");
app.use(express.json());
app.use("/aws-s3-upload-image", s3UploadImageRoute);
app.use("/", rootRoute);
app.use("/*", (req,res)=>{
res.render("404");
})
app.use(errorHandler);
app.listen(PORT, () => console.log(`Server is running on ${PORT} `));
Frontend Setup for Image Uploads
In this section, we will create a UI where user can choose image to upload. Once the upload is successful, user will get the image link in markdown format ([alt text](image_url)
) and button to copy the URL.
<div class="image-upload-toolbar">
<label title="Upload image" class="upload-image"><svg class="icons"
style="width:20px;height:20px; margin:3px 0 0 2px" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path fill="currentColor"
d="M20 5H4v14l9.292-9.294a1 1 0 011.414 0L20 15.01V5zM2 3.993A1 1 0 012.992 3h18.016c.548 0 .992.445.992.993v16.014a1 1 0 01-.992.993H2.992A.993.993 0 012 20.007V3.993zM8 11a2 2 0 110-4 2 2 0 010 4z">
</path>
</svg>
<input type="file" id="image-upload-field" accept="image/*"></label>
<div style="width: 90%; display:flex; align-items: center" aria-live="polite">
<input data-testid="markdown-copy-link" type="text" style="width:80%; max-width:360px;" id="image-url"
readonly="" placeholder="Select image to get image url" value="">
<button type="button" id="copy-btn" title="Copy" class="btn btn-outline" style="display: none;">
<svg class="icons" style="width:20px;height:20px; margin-right:5px;" id="copy-icon" viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
<path fill="currentColor"
d="M7 6V3a1 1 0 011-1h12a1 1 0 011 1v14a1 1 0 01-1 1h-3v3c0 .552-.45 1-1.007 1H4.007A1 1 0 013 21l.003-14c0-.552.45-1 1.007-1H7zm2 0h8v10h2V4H9v2zm-2 5v2h6v-2H7zm0 4v2h6v-2H7z">
</path>
</svg>
</button>
</div>
</div>
In above code, we have<input type="file">
with SVG
image icon that allows users to upload an image, input
field to display the URL of the uploaded image or error message if there is any and a button
with copy icon for coping the image URL to the clipboard once the image is uploaded.
Now, we will write some javascript code for uploading the selected image to the backend, displaying the image URL in the text input field, allowing users to copy the URL to the clipboard and handling errors if there is any.
<script>
const copyButton = document.getElementById("copy-btn");
const url = document.getElementById("image-url");
const imageInput = document.getElementById("image-upload-field");
const uploadImage = async (URL, file) => {
const formData = new FormData();
formData.append("s3Image", file);
const res = await fetch(URL, {
method: "post",
body: formData
})
const result = await res.json();
return result;
}
imageInput.addEventListener("change", async (e) => {
copyButton.style.display = "none";
const selectedImage = e.target.files[0];
if (!selectedImage) {
url.value = "No image selected";
return
}
url.value = "Uploading...";
try {
const result = await uploadImage("/aws-s3-upload-image", selectedImage);
if (!result?.success) {
throw new Error(result?.message || "Failed to upload")
}
url.value = `![Image description](${result?.imageURL})`
copyButton.style.display = "block";
} catch (error) {
url.value = error.message
}
})
copyButton.addEventListener("click", async () => {
const urlValue = url?.value;
try {
await navigator.clipboard.writeText(urlValue);
url.select();
} catch (er) {
console.log(er.message)
}
})
</script>
Conclusion
Adding image upload functionality to a web app is a powerful feature that enhances user experience and allows for richer content creation. By integrating AWS S3 with Node.js, Multer, and a user-friendly front end, I've streamlined the process of uploading images, generating URLs, and embedding them into blog posts. This approach not only simplifies image handling but also makes your web app more versatile and user-friendly. I hope this guide helps you implement similar functionality in your projects. Happy coding!
Find me on LinkedIn