Node.jsCloudinaryTutorial

Upload Multiple Images to Cloudinary with Node and Express

Cloudinary's uploader takes one image at a time. Here is the loop-and-promise pattern that gets a whole batch up, with Multer handling the incoming files. From 2019.

Temitayo Adeeyo3 min read

From the archive

An early tutorial from my first years of writing in public, preserved as part of the record. The engineer who wrote it was still learning out loud. Some practices here are not how I would build today, and that's rather the point.

Hello there. In this post we're going to build a Node.js backend that uses the Cloudinary library to upload multiple images at once. Cloudinary's uploader doesn't take multiple images by default, so we're going to find a way around it. Brace yourself, I hope you're as excited as I am.1

Requirements

Project setup

Create the project folder and initialize it.

mkdir cloud_multiple_image
cd cloud_multiple_image
npm init

In the root we need a server.js file as the entry point, a helper folder for the upload helper, and a user folder for the model, controller, and route.

server.js
const express = require('express');
const mongoose = require('mongoose');
const logger = require('morgan');
const bodyParser = require('body-parser');
const user_route = require('./user/user.route');
 
const db = 'mongodb://localhost:27017/yourdatabaseurl';
const app = express();
const port = process.env.PORT || 9000;
 
mongoose
  .connect(db, { useNewUrlParser: true, useUnifiedTopology: true })
  .then(() => console.log('connected to mongoDB', db))
  .catch((err) => console.log('error mongodb', err));
 
app.use(logger('dev'));
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: false }));
app.use(user_route);
 
app.listen(port, () => console.log('listening on port', port));

The upload helper

The trick to "multiple uploads" is realizing you don't need Cloudinary to support them, you need a single-file upload you can await in a loop. So we wrap the uploader in a promise.

helper/helper.js
const cloudinary = require('cloudinary');
 
function upload(file) {
  cloudinary.config({
    cloud_name: 'your cloud name',
    api_key: 'your api key',
    api_secret: 'your api secret',
  });
 
  return new Promise((resolve, reject) => {
    cloudinary.v2.uploader.upload(file, { width: 50, height: 50 }, (err, res) => {
      if (err) return reject(err);
      return resolve(res.url); // [!code highlight]
    });
  });
}
 
module.exports.upload = upload;

The user model

Each user document keeps an array of image URLs.

user/user.model.js
const mongoose = require('mongoose');
const Schema = mongoose.Schema;
 
const userSchema = new Schema({
  name: { type: String },
  multiple_image: { type: [] },
});
 
const user = mongoose.model('user', userSchema, 'user');
module.exports = user;

The controller

Here's where it all comes together. Multer (in the route, next section) has already written the incoming files to disk; we loop over them, await each Cloudinary upload, collect the URLs, and clean up the local files as we go.

user/user.controller.js
const user_model = require('./user.model');
const fs = require('fs');
const { upload } = require('../helper/helper');
 
exports.create = async (req, res, next) => {
  if (!req.files || req.files.length === 0) {
    return res.status(400).json({ status: false, message: 'No file uploaded' });
  }
 
  try {
    const urls = [];
    for (const file of req.files) { // [!code highlight]
      const url = await upload(file.path); // one at a time, awaited // [!code highlight]
      urls.push(url);
      fs.unlinkSync(file.path); // remove the local temp file
    }
 
    const new_user = new user_model({ ...req.body, multiple_image: urls });
    const saved = await new_user.save();
    return res.json(saved);
  } catch (e) {
    return next(e);
  }
};
 
exports.find = (req, res, next) => {
  user_model.find().then((found) => {
    return res.status(200).json({ status: true, data: found });
  });
};

The route

Multer receives the multipart form data, stores the files in an Uploads folder, and hands the controller an array via upload.array, up to six images per request here.

user/user.route.js
const user_controller = require('./user.controller');
const multer = require('multer');
const express = require('express');
const router = express.Router();
 
const storage = multer.diskStorage({
  destination: function (req, file, callback) {
    callback(null, './Uploads/');
  },
  filename: function (req, file, callback) {
    callback(null, file.originalname);
  },
});
 
const upload = multer({
  storage: storage,
  limits: { fileSize: 1000000 * 1000 },
});
 
router.post('/create', upload.array('multiple_image', 6), user_controller.create); // [!code highlight]
router.get('/find', user_controller.find);
 
module.exports = router;

Testing

Send a POST /create request as multipart form data with a name field and up to six files under the multiple_image key. The response is the saved user with an array of Cloudinary URLs. GET /find returns every user with their uploaded image URLs.

That's the whole trick. One promise-wrapped uploader, one loop. The full source code is on GitHub.

Footnotes

  1. Originally published on my Hashnode blog in November 2019. Revised for this site. Code in text instead of screenshots, and the Cloudinary credentials should of course live in environment variables, a courtesy my 2019 self extends to you now.