DEV Community

Cover image for Integrating Cloud Storage for Image Uploads
Amnish Singh Arora
Amnish Singh Arora

Posted on

Integrating Cloud Storage for Image Uploads

In my last post, I discussed about my progress in figuring out how to integrate Cloudinary for image storage in a project built using MERN stack.

If you haven't read that yet, I highly recommend going through that first for full context.

In this post, I'll discuss how I was successfully able to execute my plan and migrate an application's image storage from disk to cloud.

Table of Contents

ย 1. Configuring Cloudinary
ย 2. Modifying Route Handlers
ย 3. Updating the UI
ย 4. Testing the changes ๐Ÿงช
ย 5. The Pull Request
ย 6. Conclusion ๐ŸŽŠ

Configuring Cloudinary

Even though I already discussed in last post how I was able to upload a test image to cloudinary, that was not the best way to do it.

Which is why, I moved it to a new file and named it cloudinary.js.

cloudinary.js

This is where I configure the SDK with credentials, and expose the uploadImage method that uses the configured object.

// controllers/cloudinaryController.js
import cloudinary from 'cloudinary';
import dotenv from 'dotenv';

dotenv.config();

// Configure Cloudinary with your credentials
cloudinary.v2.config({
  cloud_name: process.env.CLOUDINARY_CLOUD_NAME,
  api_key: process.env.CLOUDINARY_API_KEY,
  api_secret: process.env.CLOUDINARY_API_SECRET,
});

// Controller function to handle image upload
const uploadImage = async (req, res, next) => {
    try {
      // Use Cloudinary SDK to upload the image
      // The buffer data we are using from request object was attached by multer
      await cloudinary.v2.uploader.upload_stream(
        { resource_type: 'image' }, // Specify resource type if necessary
        async (error, result) => {
          if (error) {
            console.error(error);
            return res.status(500).json({ error, message: "Image upload failed" });
          }

          // Save the public_id or URL in your database
          const picturePath = result.secure_url;

          // Attach the picturePath to the request
          // This will be saved in the User object when saving to the database
          req.picturePath = picturePath;

          // Continue to the next middleware or route handler
          next();
        }
      ).end(req.file.buffer);
    } catch (error) {
      console.error(error);
      res.status(500).json({ error, message: 'Image upload failed' });
    }
};

export const cloudinaryController = {
    uploadImage,
}
Enter fullscreen mode Exit fullscreen mode

The uploadImage method is meant to be used as a middleware function, that takes the image blob processed by multer middleware, upload it to cloudinary, and attach the URI of the uploaded image resource to the request object. This url of the uploaded image can later be used by the route handler.

Modifying Route Handlers

Now that I had cloudinary correctly configured, it was time to go and modify the route handlers.

Previously, it was saving the image data locally on the disk as discussed in last post.

Here's a recap!
Old way

In order to save images to cloudinary though, I needed several adjustments.

The first thing I did was configure multer such that the images were stored in memory instead of writing to disk.

// Store files in memory for Cloudinary upload later
const storage = multer.memoryStorage(); 
const upload = multer({ storage });
Enter fullscreen mode Exit fullscreen mode

Now, once the image has been parsed and loaded into memory, we can run the request object through the uploadImage middleware we talked about above.

/* ROUTES WITH FILES */
app.post("/auth/register", upload.single("picture"), cloudinaryController.uploadImage, register);
app.post("/posts", verifyToken, upload.single("picture"), cloudinaryController.uploadImage, createPost);
Enter fullscreen mode Exit fullscreen mode

The request object is piped through all the middleware functions where it is accessed/modified and certain operations happen.

I've create a diagram to better explain the flow

Request Flow

After the uploadImage function uploads the image to cloudinary, and the url of resource is attached, the request object goes to the register method, taking an example of the /auth/register route.

Here, I updated the User data to store the correct picturePath which would be the cloudinary url in this case.

/* REGISTER USER */
export const register = async (req, res) => {
  try {
    const {
      firstName,
      lastName,
      email,
      password,
      friends,
      location,
      occupation,
    } = req.body;
    const picturePath = req.picturePath;

    const salt = await bcrypt.genSalt();
    const passwordHash = await bcrypt.hash(password, salt);

    const newUser = new User({
      firstName,
      lastName,
      email,
      password: passwordHash,
      picturePath,
      friends,
      location,
      occupation,
      viewedProfile: Math.floor(Math.random() * 10000),
      impressions: Math.floor(Math.random() * 10000),
    });
    const savedUser = await newUser.save();
    res.status(201).json(savedUser);
  } catch (err) {
    res.status(500).json({ error: err.message });
  }
};
Enter fullscreen mode Exit fullscreen mode

And I repeated the same process for create posts functionality.

Updating the UI

Now that the Server was correctly setup to use cloud storage for images, I had to make a minor tweak to the UI to use the images from cloudinary instead of local storage of server.

Taking UserImage component as an example,
I changed the src property

From:

src={`https://chatroomapi-v1.onrender.com/assets/${image}`}
Enter fullscreen mode Exit fullscreen mode

To:

// image is the cloudinary url here
src={image}
Enter fullscreen mode Exit fullscreen mode

Apart from that, I noticed that the maintainer was using hard-coded strings for API_URL, which isn't a good practice. So I replaced all those instances with an environment variable to make my local testing easier as well.

Here's the commit for that
Use environment variable for API url

Testing the changes ๐Ÿงช

Its not like I tested everything after writing all the code. I was testing throughout the entire process at each step, while debugging any issues.

But, to keep the blog short, I'll just demonstrate the working state of the application.

I have already made a post that successfully used cloudinary at all stages from frontend to backend.

My profile page

But let's walk through the process one more time for a demo.

Creating a Post

Creating a Post

Successfully Created

Successfully Created

Viewing the Post

Viewing the Post

Resources on Cloudinary

Resources on Cloudinary

Sweet!

The Pull Request

At this point, everything was working as expected throughout the application and it was time to make my pull request ready for review.

Pull Request

And it was merged the very next day!

Conclusion ๐ŸŽŠ

In this post, I shared my experience integrating cloud storage for images in a MERN project, that I started planning since this post.
Throughout the semester, I've worked on several open-source contributions, and all of them gave me valuable experience about how the open source community works.
But, this was the only one where I actually learned someting new and useful. Not only that, I worked on both frontend and backend together this time, making it an even worthwhile experience overall.

Hope you enjoyed reading!
And I'll see you soon in another one of my posts.

Top comments (0)