How to Add Images and pdf files to your Node.js Application Using Cloudinary and Multer

·

10 min read

Introduction

If you are making an application, either a social media application or a library app or a blog app or any application that requires users to upload images or pdfs, you need a file upload server.

In this article, you will learn how to upload images and pdfs using Multer and Cloudinary.

What are Multer and Cloudinary?

Multer is a node.js middleware that handles file uploads while Cloudinary is used to store and host images

You will also learn how to manipulate your forms that make uploading images and pdf files optional, that is, a user can make a post without making any uploads.

Prerequisites

Before we move on, you need to have the following-:

  • A Good understanding of Web Development, that is HTML, CSS and Javascript

  • A Basic understanding of Node.js and Express

  • An Understanding of HTTP request methods in Express

  • A MongoDB Instance running on your machine and primary knowledge of it.

Setting up your project

Create or sign in to your Cloudinary account. If you are using Cloudinary for the first time, on your dashboard you should see your account details containing the cloud name, API key, API secret, and environment variable.

Create a new folder by running the following commands on your terminal

mkdir upload_images_and_pdf
cd upload_images_and_pdf

Install all Dependencies

Install Express, Multer, Mongoose, Cloudinary, dotenv, express-session, connect-mongo, router and ejs template by running the following command on your terminal.

npm install express multer cloudinary mongoose router ejs express-session connect-mongo dotenv  --save

Create a config folder in the root directory and create a .env and database.js file inside of it. The .env file will house our environment variables while the database.js file will house our database string.

mkdir config
cd config
touch .env
touch database.js

Copy all the variable names in your Cloudinary dashboard and paste them inside the .env file.

Add the variable as key = value

In .env file, add the following

PORT = 3000 //(can be any port example: 4022)
CLOUD_NAME = your cloudinary cloud name
API_KEY = your cloudinary api key
API_SECRET = your cloudinary api secret

// Your MongoDB string
DB_STRING = your database URI

Connect your mongoose package to the database string in the .env file by adding the following codes to the database.js file.

In the database.js file

const mongoose = require ('mongoose')
mongoose.set('strictQuery', true);
const connectDB = async () => {
    try{
        const conn = await mongoose.connect(process.env.DB_STRING, {
            useNewUrlParser: true,
            useUnifiedTopology: true,
        })
        console.log(`MongoDB Connected Sucessfully, Catch it real fast : ${conn.connection.host}`)

    } catch(err) {
        console.error(err)
        process.exit(1)
    }
}

module.exports = connectDB

Create a new file server.js in the root directory where we will write some configuration code for our express module, require and configure the dotenv package like this: require('dotenv').config(),

In server.js

const express = require('express')  
const app = express() //connect application to express server
const session = require("express-session");
const MongoStore = require("connect-mongo");
const connectDB = require("./config/database");
const dotenv = require('dotenv')
dotenv.config({path: './config/.env'}) //database string

//Use .env file in config folder
require("dotenv").config({ path: "./config/.env" });

//Connect To Database
connectDB();

//Using EJS for views
app.set("view engine", "ejs");

//Body Parsing
app.use(express.urlencoded({ extended: true }));
app.use(express.json());



// Sessions
app.use(
    session({
      secret: process.env['DB_STRING'],
      resave: false,
      saveUninitialized: false,
      store: MongoStore.create({
        mongoUrl: process.env['DB_STRING'],

    })

})

);


//Server Running
app.listen(process.env.PORT, () => {
  console.log("Server is running, you better catch it!");
});

The Express body parser as seen in the above code is an npm library used to process data sent through an HTTP request body.

Run your Server

Test your code with an npm start to see if it works. Make sure you are running the npm start command in the root directory.

Your response in the terminal should read like this

> start

> node server.is

Server is running, you better catch it! MongoDB Connected Sucessfully, Catch it real fast

Create the Middlewares

Create a middleware folder in the root directory. Add the cloudinary.js and multer.js files in it respectively

mkdir middleware

On the middleware folder, create a cloudinary.js file

cd middleware
touch cloudinary.js

Configure the cloudinary package to use our cloudinary credentials by adding the following code to the cloudinary.js file

In the ./middleware/cloudinary.js


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

require("dotenv").config({ path: "./config/.env" });

cloudinary.config({
  cloud_name: process.env.CLOUD_NAME,
  api_key: process.env.API_KEY,
  api_secret: process.env.API_SECRET,
});

module.exports = cloudinary;

On the middleware folder, create another file multer.js file

touch multer.js

Configure multer as a middleware to upload our files by adding the following code to the just created multer.js file

In ./middleware/multer.js

const multer = require("multer");
const path = require("path");

module.exports = multer({

//destination of storage; tells multer where to store the files
  storage: multer.diskStorage({}),

//function to control which files are acceptable; the different image and pdf file format 
  fileFilter: (req, file, cb) => {
    let ext = path.extname(file.originalname);
    if (ext !== ".jpg" && ext !== ".jpeg" && ext !== ".png" && ext !== ".pdf" && ext !== ".doc") {
      cb(new Error("File type is not supported"), false);
      return;
    }
    cb(null, true);
  },
});

Create a Routes folder in the root directory and add the main.js and upload.js files to it

mkdir routes
cd routes
touch main.js
touch upload.js

Set up the Routes paths in the server.js file

In the ./server.js

//Add Routes
const mainRoutes = require('./routes/main')
const uploadRoutes = require('./routes/upload')


app.use("/", mainRoutes);
app.use("/post", uploadRoutes);

So your server.js file should look like this

const express = require('express')  
const app = express() //connect application to express server
const session = require("express-session");
const MongoStore = require("connect-mongo");

//Add Routes
const mainRoutes = require('./routes/main')
const uploadRoutes = require('./routes/upload')


const connectDB = require("./config/database");
const dotenv = require('dotenv')
dotenv.config({path: './config/.env'}) //database string

//Use .env file in config folder
require("dotenv").config({ path: "./config/.env" });

//Connect To Database
connectDB();

//Using EJS for views
app.set("view engine", "ejs");

//Body Parsing
app.use(express.urlencoded({ extended: true }));
app.use(express.json());



// Sessions
app.use(
    session({
      secret: process.env['DB_STRING'],
      resave: false,
      saveUninitialized: false,
      store: MongoStore.create({
        mongoUrl: process.env['DB_STRING'],

    })

})

);

app.use("/", mainRoutes);
app.use("/upload", uploadRoutes);

//Server Running
app.listen(process.env.PORT, () => {
  console.log("Server is running, you better catch it!");
});

Call the express route in the main.js file. The main.js file is where our get route will be called in addition to the relevant controllers' route.

In the routes/main.js

const express = require("express");
const router = express.Router();
//Add Controllers route here

//Add the Get Route here
....

Call the express route in the upload.js file. The upload.js file is where our post route will be called in addition to the relevant controllers' route.

In the routes/upload.js

 const express = require("express");
const router = express.Router();
//Add Controllers route here

//Add the Post Route here
....

Define the Controllers

Create a Controllers folder in the root directory and create a home.js and upload.js files in it.

mkdir controllers
cd controllers
touch home.js
touch upload.js

Now add the Controllers path to the Route files - main.js and upload.js

In ./routes/main.js

 const express = require("express");
const router = express.Router();

//Controllers Route
const homeController = require("../controllers/home");

//Get Route
router.get("/", homeController.getIndex);
router.get("/uploadFile", homeController.getUploadFile);


module.exports = router;

In ./routes/upload.js

 const express = require("express");
const router = express.Router();
const upload = require("../middleware/multer");

//Controllers Route
const uploadController = require("../controllers/upload");

//POST Route
router.get("/:id", uploadController.getUploadedFile);
router.post("/uploadFile", uploadImageAndPdf.single("file"), uploadController.uploadFile);



module.exports = router;

Now define the GET request in the homeController file, that is, the home.js file inside the controllers' folder

In ./controllers/home.js

//Add Model Route

module.exports = {
    getIndex:  (req, res) => {
        try{
      res.render("index.ejs");
        } catch(err){
            console.log(err);
        }
    },


    getUploadFile: async (req, res) => {
        try{
       //Add response to model objects
    } catch(err) {

    }
}
  };

Define the post request in the upload.js file inside the Controllers folder

In ./controllers/upload.js

//the cloudinary path
const cloudinary = require("../middleware/cloudinary");
//Add the model path
//...

//the upload request
module.exports = {
uploadFile: async (req, res) => {
    try {
      // Upload image to cloudinary
      const result = await cloudinary.uploader.upload(req.file.path);

        //add response to model objects
      //...

console.log("File has been added!");
      res.redirect("/uploadFile");
    } catch (err) {
      console.log(err);
    }
  },

getUploadedFile: async (req, res) => {
    try {
//add Response to MongoDB
//...
    } catch (err) {
      console.log(err);
    }
}
}

Build the Model

Create a model folder in the root directory and add a Upload.js file inside it.

mkdir model
cd model
touch Upload.js

Now add the UploadSchema inside Upload.js file in the model folder.

In the .model/Upload.js

const mongoose = require("mongoose");

const UploadSchema = new mongoose.Schema({
  title: {
    type: String,
    required: true,
  },
  image: {
    type: String,
    require: true,
  },
  pdfUrl: {
    type: String,
    required: true
  },
  cloudinaryId: {
    type: String,
    require: true,
  },
  createdAt: {
      type: Date,
      default: Date.now
  }

});

module.exports = mongoose.model("Upload", UploadSchema);

Now Update the upload.js file in the controller folder to reflect the Models object

In ./controllers/upload.js

//the cloudinary path
const cloudinary = require("../middleware/cloudinary");
//Add the model path
const Upload = require("../model/Upload");

//the upload request
module.exports = {
uploadFile: async (req, res) => {
    try {
      // Upload image to cloudinary
      const result = await cloudinary.uploader.upload(req.file.path);

        //add response to model objects
        await Upload.create({
            title: req.body.title,
           image: result.secure_url,
           pdfUrl: result.secure_url,
            cloudinaryId: result.public_id,
          });

console.log("File has been added!");
      res.redirect("/uploadFile");
    } catch (err) {
      console.log(err);
    }
  },

getUploadedFile: async (req, res) => {
    try {
//add Response to MongoDB
        const upload = await Upload.findById(req.params.id);
      res.render("upload.ejs", { upload: upload })
    } catch (err) {
      console.log(err);
    }
}
}

In ./controllers/home.js

const Upload = require('../model/Upload');

module.exports = {
    getIndex:  (req, res) => {
        try{
      res.render("index.ejs");
        } catch(err){
            console.log(err);
        }
    },

    getUploadFile: async (req, res) => {
        try{
        const upload = await Upload.find().sort({createdAt: "desc" })
        res.render("upload.ejs", {upload: upload})
    } catch(err) {

    }
}
  };

Define the Views

Now create the views folder in the root directory. Create an index.ejs and upload.ejs file inside the views directory

mkdir views
cd views
touch index.ejs
touch upload.ejs

The index.ejs file is where the input form would be. So add the following code to the index.ejs file.

In the ./views/index.ejs

<!DOCTYPE html>
<html lang="en">
    <head>

        <meta charset="UTF-8">
        <meta http-equiv="X-UA-Compatible" content="IE=edge">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <meta name="description" content="Uploading Images and Pdf Files with Multer and Cloudinary">
        <meta name="keywords" content="upload, images, file, pdf, cloudinary, multer, mongodb">
        <!-----Title of Page------->
        <title>Upload Files with Multer and Cloudinary </title> 
    </head>
    <body>
     <!-- Section: Design Block -->
     <section class="">

          <div class ="">
            <h6 class="">   Input the Title of your Image or Pdf File</h6> 

    <form action="/upload/uploadFile" enctype="multipart/form-data" method="POST">

                    <label for=""> Title: </label>  
                  <input type="text" class="form-control" placeholder="Title of the File" name="title" id="title">
                   <br>

                    <div class="">
                   <label for="" class="form-label"> Upload Image(Optional)</label>
                   <p> <small>Add an image or Pdf here </small></p>
                   <input type="file"  class="form-control" id="imageUpload" name="file">

                   </div> 
                   <br/>
                   <div class="">

           <button type="submit" class="">
                       Upload 
                      </button>

   </div>

                </form>

                </div>
              </div>

      </section>
    </body>
    </html>

The upload.ejs file contains the template where the image or pdf file will be uploaded

In the ./views/upload.ejs

<!DocTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta name="description" content="Uploading Images and Pdf Files with Multer and Cloudinary">
    <meta name="keywords" content="upload, images, file, pdf, cloudinary, multer, mongodb">
    <!-----Title of Page------->
    <title>Upload Files with Multer and Cloudinary </title> 
</head>
<body>
<section>
         <% for (let im of upload) { %>
        <h5><%= im.title %></h5>
        <img src="<%= im.image%>" class="" style="width:50rem; height:40rem;" />
        <iframe src="<%= im.pdfUrl %>" width="100%" height="500"></iframe>
         <% } %>
      </section>
</body>
</html>

Add some Stylings

We can add Bootstrap styling to the head of our HTML to give it a better look

<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.0-beta3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-eOJMYsd53ii+scO/bJGFsiCZc+5NDVN2yr8+0RDqr0Ql0h+rP48ckxlpbzKgwra6" crossorigin="anonymous"/>

So ./views/upload.ejs looks like this

<!DOCTYPE html>
<html lang="en">
    <head>

        <meta charset="UTF-8">
        <meta http-equiv="X-UA-Compatible" content="IE=edge">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <meta name="description" content="Uploading Images and Pdf Files with Multer and Cloudinary">
        <meta name="keywords" content="upload, images, file, pdf, cloudinary, multer, mongodb">
        <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.0-beta3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-eOJMYsd53ii+scO/bJGFsiCZc+5NDVN2yr8+0RDqr0Ql0h+rP48ckxlpbzKgwra6" crossorigin="anonymous"/>
        <!-----Title of Page------->
        <title>Upload Files with Multer and Cloudinary </title> 
    </head>
    <body>
     <section>
         <% for (let im of upload) { %>
        <h5><%= im.title %></h5>
        <img src="<%= im.image%>" class="" style="width:50rem; height:40rem;" />
        <iframe src="<%= im.pdfUrl %>" width="100%" height="500"></iframe>
         <% } %>
      </section>
    </body>
    </html>

Test your Code

Run npm start in the root directory in the terminal and check http://localhost:3000/ on your browser for something like this

The form page - index.ejs on your server

Then try to upload an image from your local machine to the browser, you will get the response that "File has been added" and you will be redirected to the "../uploadFile" route where your image file has been successfully uploaded.

You can check out the source code here

However, asides from the styling of the upload page, there are several issues to be noticed from the images uploaded

i) images are uploaded twice, that is there is a duplicate copy of each image uploaded.

ii) uploading a post without an image or pdf file throws a file path error.

The second part of this article aims to resolve these errors where forms can be manipulated to upload a post without an image file and erase the duplicated file upload.