DEV Community

Madhav Pandey
Madhav Pandey

Posted on • Edited on • Originally published at p-blog.online

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.
Image description

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

  1. Login in to the AWS Management Console
  2. Navigate to S3 and click on Create bucket
  3. 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
  4. Now, we need to add a Bucket policy that allows anyone to view images located inside the bucket. For that go to the Permissions tab and edit the bucket policy. You can use Policy generator to get the bucket policy and copy and paste the policy Image description
{
    "Version": "2012-10-17",
    "Id": "Policy1725365190218",
    "Statement": [
        {
            "Sid": "Stmt1725365183685",
            "Effect": "Allow",
            "Principal": "*",
            "Action": "s3:GetObject",
            "Resource": "arn:aws:s3:::your-bucket-name/*"
        }
    ]
}
Enter fullscreen mode Exit fullscreen mode

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

  1. Go to the IAM Dashboard
  2. Go to the Policies section and click on Create policy and you will see Policy editor
  3. In the Select a service section choose S3 and search and check for PutObject in Actions allowed section
  4. In the Resources section choose Specific and click on Add ARNs Specify ARN box will open
  5. In the Speify ARN box enter your bucket name, Resource ARN check Any object name for Resource object name lastly click on Add ARNs button Image description
  6. Now we are in Review and create section. In this section give a meaningful name for this policy and click on Create policy button

Creating an IAM User

  1. Go to the Users section of IAM Dashboard
  2. Click on Create user and give a meaningful name and click on Next
  3. In the Set permissions section choose Attach policies directly and search and choose for the policy that we have created above. Lastly, click on Next button
  4. In the Review and create section just need to click on Create user button
  5. Go the the newly created user dashboard, click on Create access key and you can choose Other in Access key best practices & alternatives section. Lastly, click on Next button
  6. In Set description tag tag you may write description of this access key but it is optional and click on Create access key
  7. This is the final and important part, where we will get the Access key and Secret access key. You will see both keys in this Retrieve 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;

Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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,
  },
});



Enter fullscreen mode Exit fullscreen mode

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;

Enter fullscreen mode Exit fullscreen mode

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} `));

Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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.
Image description

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>
Enter fullscreen mode Exit fullscreen mode

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

Top comments (0)