Multer – A Sequential Approach

By on December 10th, 2019 in   Expressjs JavaScript Nodejs

In this article, I will be demonstrating how to upload files in Nodejs using Express and Multer in a manner that can be used in both linear fashion (using async / await) or asynchronously with promises. This article will follow the given sequence:

  1. Brief Introduction to Multer
  2. Conventional Usage (Middleware Or As A Method)
  3. Setbacks In The Conventional Usage
  4. My Approach
  5. Some Things To Keep In Mind
  6. Conclusion

1. Brief Introduction to Multer

Multer is one of the most popular Nodejs middleware to handle multipart / form-data as Node doesn’t have an in-built module to handle this kind of data. It is an easy way to get the job done which would otherwise be a very tedious process. (Thanks to Multer there).

Multipart/form-data is an encoding type that allows files to be sent through a POST request usually from HTML forms. It divides the form data into multiple parts and sends it in the request body to the server. In simple words, without this encoding the files cannot be sent through POST.

In PHP and other such server side scripts/ languages. Files or multipart data are automatically handled by the server when a form is submitted.

2. Conventional Usage (Middleware Or As A Method)

Multer works like follows:

  • Receive request in middleware
  • Populate data
  • Set the files as “Files” in req.body
  • Set other fields directly in req.body

Assuming we have 2 file fields in a form with names avatar and gallery respectively, the shortest code would look somewhat like this as given in the Multer Documentation:

var express = require('express')
var multer  = require('multer')
var upload = multer({ dest: 'uploads/' })

var app = express()

var cpUpload = upload.fields([{ name: 'avatar', maxCount: 1 }, { name: 'gallery', maxCount: 8 }])
app.post('/cool-profile', cpUpload, function (req, res, next) {
  // req.files is an object (String -> Array) where fieldname is the key, and the value is array of files
  //
  // e.g.
  //  req.files['avatar'][0] -> File
  //  req.files['gallery'] -> Array
  //
  // req.body will contain the text fields, if there were any
})

It can also be used as a function if required:

var multer = require('multer')
var upload = multer().fields([{ name: 'avatar', maxCount: 1 }, { name: 'gallery', maxCount: 8 }]);

app.post('/profile', function (req, res) {
  upload(req, res, function (err) {
    if (err instanceof multer.MulterError) {
      // A Multer error occurred when uploading.
    } else if (err) {
      // An unknown error occurred when uploading.
    }

    // Everything went fine.
  })
})

3. Setbacks In The Conventional Usage

While providing an efficient way to upload, Multer has its ups and downs in terms of usage at the backend. It might not even be called setbacks but to me it sure is an itch in the brain. I personally don’t like callbacks, and callbacks within callbacks making a vertical stack of endless right indents. Here are the issues I found with conventional Multer approach:

  • Lack of clear way to keep code clean and catch custom errors at same time
  • No way to use Multer in a synchronized or liner manner
  • Files available only when the form is completely uploaded
  • No way to use it with async/await or promises

If you are using Multer for the 1st time, you will most probably end up wondering where your text fields went, or where your files are (depending upon the inputs’ order).

4. My Approach

I believe in relevancy; the code should be where it belongs and do only what it is intended to do. At the same time, it should be flexible enough to be used with either promises, async / await, asynchronously or simply sequentially.
Ideally, I prefer a simple approach to create a post or data object like:

  • Create an Express Server & Ajax Form
  • Setup Multer for the form
  • Setup MVC folder structure
  • Get result of upload (error or success) in the controller
  • Proceed with checking other form fields (other validations)
  • Create / save a post / data object
  • Send success / failure to the frontend

You can find the complete demo (.zip) here.

Despite all my efforts, Multer did not let me isolate the upload functionality for async / await & reusability. I wanted to be able to do something before and after form upload and it would have been good to know when the file uploading completed. So, I came up with the following solution taking some help from stackoverflow and google:

  • Setup Multer upload middleware in a separate file
  • Wrap the middleware upload function in a new Promise so it either resolves or rejects
  • Use the Promisified function in your controller either sequentially with async/await or asynchronously

Above mentioned approach would look like this:

Project file Hierarchy:

> Demo-multer
    > models
        > post
            controller.js
            post_upload_function.js
    > public
        > css
            style.css
        > img
        > libs
            jquery & jquery form files
        > pages
            create.html
    > views
    app.js

HTML Form (create.js):

Given form contains an input field named title, a textarea named description and 2 file inputs with names featured_image and userPhoto. Rest of the elements are for styling purposes and have no role in functioning of this demo.

<form class="form_outer" id="uploadForm" enctype="multipart/form
data" action="" method="POST">
        <fieldset>
            <legend>Create Post</legend>
            Title:<br>
            <input type="text" name="title" value=""><br>
            Description:<br>
            <textarea name="description" rows="10" value=""></textarea><br>

            <div class="file_upload_box">
                <div class="file_upload_box_left">
                    <label class="myLabel">
                        <input type="file" id="featured_image" name="featured_image" value=""
                            accept="image/x-png,image/gif,image/jpeg" />
                        <span>Select Featured Image:</span>
                    </label>
                    <div class="image_preview"></div>
                </div>

            </div>

            <div class="file_upload_box">
                <div class="file_upload_box_left">
                    <label class="myLabel">
                        <input type="file" id="userPhoto" name="userPhoto" value="" multiple
                            accept="image/x-png,image/gif,image/jpeg" />
                        <span>Select Post Images:</span>
                    </label>
                    <div class="image_preview"></div>
                </div>
            </div>

            <input name="submit" id="submit" type="submit" value="Create Post" />

            <span id="status"></span>
        </fieldset>
    </form>

Controler.js

const { uploadForm } = require("./post_upload_function");

//posting data for creating of page
module.exports.postAddPost = async (req, res, next) => {

    try {
        await uploadForm(req, res);

        //Validate body fields after image has been uploaded and all fields poppulated 
        console.log("Validate Body (req.body) & Files (req.body.files)");

        //Create an object for Post DB 
        console.log("Post object created");

        //Save post in the DB
        console.log("Post Saved");

        //Any Other Step

        //Send confirmation
        res.send({ message: "Post Created Successfully", err: false, success: true });
        console.log("Post Created Successfully")

    } catch (error) {
        //catch any errors in any promise
        next(error);
    }
};

Post_upload_function.js

//importing 3rd party modules
const multer = require("multer");
const path = require("path");
const uploadDir = path.join(".", "public", "img");

//setting up destination and filenames for Multer 
const storage = multer.diskStorage({
    destination: function (req, file, callback) {
        callback(null, uploadDir);
    },
    filename: function (req, file, callback) {
        const file_name = file.fieldname + "-" + Date.now() + path.extname(file.originalname);
        callback(null, file_name);
    }
});

//setting file filter for multer
const imageFilter = function (req, file, cb) {
    // accept image only
    if (!file.originalname.match(/\.(jpg|jpeg|png|gif|JPG)$/)) {
        return cb(new Error('Only image files are allowed!'), false);
    }
    cb(null, true);
};

//setting multer field method for files acceptance
const upload = multer({ storage, fileFilter: imageFilter }).fields(
    [
        {
            name: "featured_image",
            maxCount: 1
        },
        {
            name: "userPhoto",
            maxCount: 2
        }
    ]);

//main method for uploading post data (including files) - method changed to async + promisified for linear execution
uploadForm = (req, res) => {
    return new Promise(function (resolve, reject) {
        //Do something before uploading
        upload(req, res, err => {

            //multer parse or file error
            if (err) {
                console.log({ message: "Could not upload the file (post uploader)", file: "post_uploader", err: err });
                return reject({ message: "Could not upload the file ", file: "post_uploader", err: err });
            }

            resolve();
        });

        //Do something after uploading (reject or resolve accordingly)  
    });
};

module.exports = { uploadForm }

5. Some Things To Keep In Mind

There are few things to be careful about when using Multer as mentioned in the Documentation.

If you are using middleware approach:

When using for the 1st time:

When Setting up Multer:

6. Conclusion

In the world of development, there can be many approached to accomplish any task. I showed you mine as it suits my needs. Let me know how you would use Multer in your app.

Also check out my other articles if you like approach-based programming. Also, refer to my article on Form With Image Previews if you want to see how the form in the demo was made.
That is it for now. Let me know if you have any questions or need something doesn’t work out for you in the above example.

Leave a Reply

Your email address will not be published. Required fields are marked *