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
.
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,
}
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.
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 });
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);
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
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 });
}
};
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}`}
To:
// image is the cloudinary url here
src={image}
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.
But let's walk through the process one more time for a demo.
Creating a Post
Successfully Created
Viewing the Post
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.
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)