DEV Community

Cover image for Cloudinary Image Upload in Node.js: Complete Guide for 2026
Md. Maruf Rahman
Md. Maruf Rahman

Posted on • Originally published at marufrahman.live

Cloudinary Image Upload in Node.js: Complete Guide for 2026

Cloudinary is the most popular and powerful cloud-based image and video management service for Node.js applications. After building several applications that required image uploads, I realized that storing images on your own server is a terrible idea. You have to worry about storage space, bandwidth costs, image optimization, CDN setup, and scaling issues. That's when I discovered Cloudinary, and it completely changed how I handle images in production applications.

Cloudinary is a cloud-based service that handles everything related to images and videos: storage, optimization, transformation, and delivery through a global CDN. Instead of managing image files yourself, you upload them to Cloudinary and get back optimized URLs that you can use anywhere. Cloudinary automatically optimizes images, generates different sizes, and serves them through a fast CDN.

πŸ“– Want the complete guide with more examples and advanced patterns? Check out the full article on my blog for an in-depth tutorial with additional code examples, troubleshooting tips, and real-world use cases.

What is Cloudinary?

Cloudinary is a cloud-based image and video management service that provides image upload, storage, optimization, transformation, and CDN delivery. Cloudinary is the most popular solution for handling media files in web applications without managing your own image infrastructure.

Why use Cloudinary?

  • Automatic image optimization and compression
  • On-the-fly image transformations (resize, crop, filters)
  • Global CDN for fast image delivery
  • No need to manage your own image infrastructure
  • Free tier with 25 GB storage and 25 GB monthly bandwidth
  • Industry standard for image management in Node.js

Installation

Installing Cloudinary is straightforward:

npm install cloudinary
Enter fullscreen mode Exit fullscreen mode

Configuration

Setting up Cloudinary config is essential for using Cloudinary in your Node.js application:

const cloudinary = require("cloudinary").v2;
const fs = require("fs");
const path = require("path");

cloudinary.config({
  cloud_name: process.env.CLOUDINARY_CLOUD_NAME,
  api_key: process.env.CLOUDINARY_API_KEY,
  api_secret: process.env.CLOUDINARY_API_SECRET,
});
Enter fullscreen mode Exit fullscreen mode

Environment Variables

Create a .env file with your Cloudinary credentials:

CLOUDINARY_CLOUD_NAME=your_cloud_name
CLOUDINARY_API_KEY=your_api_key
CLOUDINARY_API_SECRET=your_api_secret
Enter fullscreen mode Exit fullscreen mode

You can get these credentials from your Cloudinary dashboard.

Single Image Upload

Cloudinary image upload for a single image is the most common use case. Here's how to create a function to upload a single product image to Cloudinary:

async function uploadProductImageToCloudinary(filename) {
  try {
    const file = `./uploads/${filename}`;
    const uploadedImageResult = await cloudinary.uploader.upload(file);

    // Generate optimized image URL
    const optimizedImageUrl = cloudinary.url(uploadedImageResult.public_id, {
      transformation: [
        {
          quality: "auto",
          fetch_format: "auto",
        },
        {
          width: 400,
          height: 400,
          crop: "fill",
          gravity: "auto",
        },
      ],
    });

    const imageData = {
      optimizedUrl: optimizedImageUrl || "",
      secureUrl: uploadedImageResult?.secure_url || "",
      publicId: uploadedImageResult?.public_id || "",
      url: uploadedImageResult?.url || "",
    };

    // Delete local file after upload
    try {
      fs.unlinkSync(path.join(__dirname, "..", "uploads", filename));
    } catch (error) {
      console.log("Error deleting local image", error);
    }

    return imageData;
  } catch (error) {
    console.log("Error uploading image to Cloudinary", error);
    return null;
  }
}
Enter fullscreen mode Exit fullscreen mode

Uploading from Buffer

If you're using Multer with memory storage, you can upload directly from a buffer:

async function uploadImageFromBuffer(buffer, options = {}) {
  return new Promise((resolve, reject) => {
    const uploadStream = cloudinary.uploader.upload_stream(
      {
        folder: options.folder || "uploads",
        transformation: [
          { quality: "auto", fetch_format: "auto" },
          { width: 800, height: 800, crop: "fill" },
        ],
      },
      (error, result) => {
        if (error) reject(error);
        else resolve(result);
      }
    );
    uploadStream.end(buffer);
  });
}

// Usage with Multer
router.post("/upload", upload.single("file"), async (req, res) => {
  if (!req.file) {
    return res.status(400).json({ error: "No file uploaded" });
  }

  try {
    const result = await uploadImageFromBuffer(req.file.buffer, {
      folder: "products",
    });

    res.json({
      success: true,
      url: result.secure_url,
      publicId: result.public_id,
    });
  } catch (error) {
    res.status(500).json({ error: error.message });
  }
});
Enter fullscreen mode Exit fullscreen mode

Gallery Upload (Multiple Images)

Cloudinary image upload for multiple gallery images is essential for product galleries. Here's how to upload multiple gallery images to Cloudinary:

async function uploadProductGalleryToCloudinary(filenames) {
  if (!filenames || filenames.length === 0) {
    return [];
  }

  const galleryImages = [];

  for (const filename of filenames) {
    try {
      const file = `./uploads/${filename}`;
      const uploadedImageResult = await cloudinary.uploader.upload(file);

      const optimizedImageUrl = cloudinary.url(uploadedImageResult.public_id, {
        transformation: [
          { quality: "auto", fetch_format: "auto" },
          { width: 800, height: 800, crop: "fill", gravity: "auto" },
        ],
      });

      galleryImages.push({
        optimizedUrl: optimizedImageUrl || "",
        secureUrl: uploadedImageResult?.secure_url || "",
        publicId: uploadedImageResult?.public_id || "",
        url: uploadedImageResult?.url || "",
      });

      // Delete local file
      try {
        fs.unlinkSync(path.join(__dirname, "..", "uploads", filename));
      } catch (error) {
        console.log("Error deleting local gallery image", error);
      }
    } catch (error) {
      console.log(`Error uploading gallery image ${filename}:`, error);
    }
  }

  return galleryImages;
}
Enter fullscreen mode Exit fullscreen mode

Parallel Upload for Better Performance

For better performance, you can upload multiple images in parallel:

async function uploadGalleryParallel(filenames) {
  const uploadPromises = filenames.map(async (filename) => {
    try {
      const file = `./uploads/${filename}`;
      const result = await cloudinary.uploader.upload(file, {
        transformation: [
          { quality: "auto", fetch_format: "auto" },
          { width: 800, height: 800, crop: "fill" },
        ],
      });

      // Delete local file
      fs.unlinkSync(path.join(__dirname, "..", "uploads", filename));

      return {
        optimizedUrl: cloudinary.url(result.public_id, {
          transformation: [{ quality: "auto" }],
        }),
        secureUrl: result.secure_url,
        publicId: result.public_id,
      };
    } catch (error) {
      console.log(`Error uploading ${filename}:`, error);
      return null;
    }
  });

  const results = await Promise.all(uploadPromises);
  return results.filter((result) => result !== null);
}
Enter fullscreen mode Exit fullscreen mode

Delete Images from Cloudinary

Deleting images from Cloudinary is important for managing storage and cleaning up unused images:

async function deleteImageFromCloudinary(publicId) {
  try {
    const deletedImage = await cloudinary.uploader.destroy(publicId, {
      resource_type: "image",
    });
    console.log("Deleted image from Cloudinary:", deletedImage);
    return true;
  } catch (error) {
    console.log("Error deleting image from Cloudinary", error);
    return false;
  }
}

// Delete multiple images
async function deleteMultipleImagesFromCloudinary(publicIds) {
  if (!publicIds || publicIds.length === 0) {
    return true;
  }

  try {
    const results = await cloudinary.api.delete_resources(publicIds, {
      resource_type: "image",
    });
    console.log("Deleted images from Cloudinary:", results);
    return true;
  } catch (error) {
    console.log("Error deleting multiple images from Cloudinary", error);
    return false;
  }
}
Enter fullscreen mode Exit fullscreen mode

Complete Integration Example

Integrating Cloudinary image upload in product creation is a common pattern. Here's a complete example using Multer for file handling:

const {
  uploadProductImageToCloudinary,
  uploadProductGalleryToCloudinary,
} = require("../utils/cloudinaryHelper");
const multer = require("multer");

// Multer setup (see my [Multer guide](https://marufrahman.live/blog/multer-file-upload-express) for details)
const upload = multer({
  storage: multer.diskStorage({
    destination: "./uploads",
    filename: (req, file, cb) => {
      const uniqueSuffix = Date.now() + "-" + Math.round(Math.random() * 10000);
      cb(null, file.fieldname + "-" + uniqueSuffix + path.extname(file.originalname));
    },
  }),
  limits: { fileSize: 5 * 1024 * 1024 }, // 5MB
});

// Product controller
async function createProduct(req, res) {
  try {
    const newProduct = await productModel.create(req.body);

    // Upload main product image to Cloudinary
    if (req.files.product_image && req.files.product_image.length > 0) {
      const mainImageData = await uploadProductImageToCloudinary(
        req.files.product_image[0].filename
      );
      if (mainImageData) {
        await productModel.update(newProduct.id, {
          product_image_optimizedUrl: mainImageData.optimizedUrl,
          product_image_secureUrl: mainImageData.secureUrl,
          product_image_publicId: mainImageData.publicId,
        });
      }
    }

    // Upload gallery images
    if (req.files.product_gallery && req.files.product_gallery.length > 0) {
      const galleryFilenames = req.files.product_gallery.map((file) => file.filename);
      const galleryImages = await uploadProductGalleryToCloudinary(galleryFilenames);
      if (galleryImages.length > 0) {
        await productModel.update(newProduct.id, {
          product_gallery: galleryImages,
        });
      }
    }

    const updatedProduct = await productModel.getById(newProduct.id);
    return res.status(201).json(updatedProduct);
  } catch (error) {
    console.error("Error creating product:", error);
    return res.status(500).json({
      message: "Error creating product",
      error: error.message,
    });
  }
}

// Route
router.post(
  "/products",
  upload.fields([
    { name: "product_image", maxCount: 1 },
    { name: "product_gallery", maxCount: 10 },
  ]),
  createProduct
);
Enter fullscreen mode Exit fullscreen mode

Image Transformations

Cloudinary provides powerful image transformations. Here are some common examples:

// Resize and crop
const resizedUrl = cloudinary.url(publicId, {
  width: 400,
  height: 400,
  crop: "fill",
  gravity: "auto",
});

// Apply filters
const filteredUrl = cloudinary.url(publicId, {
  effect: "grayscale",
  quality: "auto",
});

// Format conversion
const webpUrl = cloudinary.url(publicId, {
  fetch_format: "auto",
  quality: "auto",
});

// Multiple transformations
const transformedUrl = cloudinary.url(publicId, {
  transformation: [
    { width: 800, height: 600, crop: "fill" },
    { quality: "auto", fetch_format: "auto" },
    { effect: "sharpen" },
  ],
});
Enter fullscreen mode Exit fullscreen mode

Best Practices

Following Cloudinary best practices ensures optimal performance and cost efficiency:

  1. Always delete local files after uploading - Don't keep duplicate files on your server
  2. Use optimized URLs - Leverage Cloudinary's automatic optimization
  3. Store public_id - Keep the public_id for easy deletion later
  4. Implement error handling - Handle upload failures gracefully
  5. Use transformations - Apply consistent sizing and formatting
  6. Clean up old images - Delete unused images when updating products
  7. Use folders - Organize images in folders for better management
  8. Set appropriate quality - Use quality: "auto" for automatic optimization
  9. Use CDN URLs - Always use secure_url for HTTPS delivery
  10. Monitor usage - Keep track of your Cloudinary usage to avoid surprises

Security Best Practices

  • Never expose your API secret in client-side code
  • Use signed URLs for private images
  • Implement rate limiting for upload endpoints
  • Validate file types before uploading
  • Set appropriate file size limits
  • Use environment variables for credentials

Common Use Cases

Product Image Management

Perfect for e-commerce applications where you need to manage product images with galleries:

// Upload product with main image and gallery
const product = {
  name: "Product Name",
  price: 99.99,
  mainImage: mainImageData.secureUrl,
  gallery: galleryImages.map(img => img.secureUrl),
  imagePublicIds: {
    main: mainImageData.publicId,
    gallery: galleryImages.map(img => img.publicId),
  },
};
Enter fullscreen mode Exit fullscreen mode

User Profile Pictures

Upload and optimize user profile pictures:

async function uploadProfilePicture(userId, imageFile) {
  const result = await cloudinary.uploader.upload(imageFile, {
    folder: `users/${userId}`,
    transformation: [
      { width: 200, height: 200, crop: "fill", gravity: "face" },
      { quality: "auto" },
    ],
  });

  return {
    url: result.secure_url,
    publicId: result.public_id,
  };
}
Enter fullscreen mode Exit fullscreen mode

Resources and Further Reading

Conclusion

Cloudinary provides a robust solution for image management in Node.js applications. With automatic optimizations, transformations, and CDN delivery, Cloudinary is perfect for product image management in inventory systems. The Cloudinary image upload integration is straightforward and provides excellent performance for image-heavy applications.

Key Takeaways:

  • Cloudinary is the industry standard for cloud-based image management
  • Automatic image optimization reduces bandwidth and improves performance
  • On-the-fly transformations eliminate the need for multiple image versions
  • Global CDN ensures fast image delivery worldwide
  • Free tier is great for development and small projects
  • Always delete local files after uploading to Cloudinary
  • Store public_id for easy image deletion later
  • Use optimized URLs for better performance

Whether you're building a simple image upload feature or a complex system with Cloudinary image upload for galleries, transformations, and optimization, this guide provides the foundation you need. Cloudinary makes image management simple, secure, and efficient.


What's your experience with Cloudinary? Share your tips and tricks in the comments below! πŸš€


πŸ’‘ Looking for more details? This is a condensed version of my comprehensive guide. Read the full article on my blog for additional examples, advanced patterns, troubleshooting tips, and more in-depth explanations.

If you found this guide helpful, consider checking out my other articles on Node.js development and backend development best practices.

Top comments (0)