Node.js Image Uploader using Express and jQuery Ajax

Image upload is one of the most useful and required feature for almost any kind of website. From Profile picture upload to Gallery and Photos Albums and many others features of websites require the image upload mechanism. Node Express is one of the growing Web Framework that is used to build websites using JavaScript. We’ll see how we can implement the image upload feature using these technologies.


Note: The demo might take a second or two to load up sometimes, since the dyne might be at sleep.

To build this feature, we’ll use,


These packages are used at the backend, in the front-end of the web application Twitter Bootstrap and jQuery is used.

Directory Structure

The application has the following structure,

  • node-image-upload
    • node_modules - Contains the node packages.
    • tmp_uploads - Default upload directory.
    • uploads - Public accessible images directory.
    • app.js - Contains the Node Express server code.
    • public - Contains static asset files.
      • assets
        • js
          • app.js - Contains the fron-end ajax file upload code.
    • views - Contains the view templates/files.
      • index.html - Main index HTML file.


There is also a package.json file that contains the info about the project and it’s dependencies.

Front-end Markup

First all the required libraries are required and the app.js script, which is empty for now.

<!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">
    <title>Codedodle - Node.js image upload</title>
    <meta name="description" content="Node.js image upload using express, jQuery using ajax." />
    <!-- Latest compiled and minified CSS -->
    <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous">
    <style type="text/css">body {background-color: #cccccc;}</style>
</head>
<body>
    
    <!-- jQuery (necessary for Bootstrap's JavaScript plugins) -->
    <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.12.4/jquery.min.js"></script>
    <!-- Latest compiled and minified JavaScript -->
    <script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js" integrity="sha384-Tc5IQib027qvyjSMfHjOMaLkfuWVxZxUPnCJA7l2mCWNIpG9mGCD8wGNIcPD7Txa" crossorigin="anonymous"></script>
    <script type="text/javascript" src="/assets/js/app.js"></script>
</body>
</html>

Let’s add the components that is needed for creating the application.

Upload Form

<!-- Photos upload Form -->
<form id="upload-photos" method="post" action="/upload_photos" enctype="multipart/form-data">
    <div class="form-group">
        <label for="photos-input">File input</label>
        <input id="photos-input" type="file" name="photos[]" multiple="multiple" >
        <p class="help-block">You can upload up to 3 files.</p>
    </div>
    <input type="hidden" name="csrf_token" value="just_a_text_field" />
    <input class="btn btn-default" type="submit" name="Photo Uploads" value="Upload Photos" />
</form>

The Upload Form is the main component in the page. This is how the users selects the files to upload and other inputs. There is a hidden input field csrf_token to demonstrate how other fields can be present in the form along with the file input. Nothing is done with that input though.

The file input has the attribute multiple=“multiple” which accepts multiple input files.

Progress Bar

<!-- Progress Bar -->
<div class="row">
    <div class="col-md-12">
        <div class="progress">
            <div class="progress-bar progress-bar-striped active" role="progressbar" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100" style="width: 0%">
                <span class="sr-only"></span>
            </div>
        </div>
    </div>
</div>

Next component in the page is the Progress Bar. This is a Bootstrap component which can be used directly without much code. To show the progress in the component, just adjust the width css property of the element using jQuery.

Photos Container

<!-- Photos Album Container -->
<div id="album" class="row"></div>

This is where the photos appear after the upload has been finished successfully.

Complete markup,

index.html
<!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">
    <title>Codedodle - Node.js image upload</title>
    <meta name="description" content="Node.js image upload using express, jQuery using ajax." />
    <!-- Latest compiled and minified CSS -->
    <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous">
    <style type="text/css">body {background-color: #cccccc;}</style>
</head>
<body>
    <div class="container">
        <div class="row">
            <div class="col-md-4"></div>
            <div class="col-md-4">
                <!-- Photos upload Form -->
                <form id="upload-photos" method="post" action="/upload_photos" enctype="multipart/form-data">
                    <div class="form-group">
                        <label for="photos-input">File input</label>
                        <input id="photos-input" type="file" name="photos[]" multiple="multiple" >
                        <p class="help-block">You can upload up to 3 files.</p>
                    </div>
                    <input type="hidden" name="csrf_token" value="just_a_text_field" />
                    <input class="btn btn-default" type="submit" name="Photo Uploads" value="Upload Photos" />
                </form>
                <br/>
                <!-- Progress Bar -->
                <div class="row">
                    <div class="col-md-12">
                        <div class="progress">
                            <div class="progress-bar progress-bar-striped active" role="progressbar" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100" style="width: 0%">
                                <span class="sr-only"></span>
                            </div>
                        </div>
                    </div>
                </div>
            </div>
            <div class="col-md-4"></div>
        </div>
        <!-- Photos Album Container -->
        <div id="album" class="row"></div>
    </div>
    <!-- jQuery (necessary for Bootstrap's JavaScript plugins) -->
    <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.12.4/jquery.min.js"></script>
    <!-- Latest compiled and minified JavaScript -->
    <script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js" integrity="sha384-Tc5IQib027qvyjSMfHjOMaLkfuWVxZxUPnCJA7l2mCWNIpG9mGCD8wGNIcPD7Txa" crossorigin="anonymous"></script>
    <script type="text/javascript" src="/assets/js/app.js"></script>
</body>
</html>

Frontend Ajax Image Upload

Now to handle the file upload from the form and send those files using Ajax to the backend.

Handling Form Submission

// On form submit, handle the file uploads.
$('#upload-photos').on('submit', function (event) {
    event.preventDefault();

    // Get the files from input, create new FormData.
    var files = $('#photos-input').get(0).files,
        formData = new FormData();

    if (files.length === 0) {
        alert('Select atleast 1 file to upload.');
        return false;
    }

    if (files.length > 3) {
        alert('You can only upload up to 3 files.');
        return false;
    }

    // Append the files to the formData.
    for (var i=0; i < files.length; i++) {
        var file = files[i];
        formData.append('photos[]', file, file.name);
    }

    // Note: We are only appending the file inputs to the FormData.
    uploadFiles(formData);
});

In the above script, an on submit event listener is binded to the form element. When the form is submitted, the default form submission action is prevented by calling the event.preventDefault() method, which prevents the form submission being sent to the server.

The input files is retrieved from the input file field using jQuery and a new FormData is created. To know more about FormData visit here.

Then the input file field is checked if any files are submitted and there not more than 3 file submitted for upload. Once the check is passed, we append the file inputs to the formData Object we created before and pass it to the uploadFiles function to send them to the server.

Let’s look at the uploadFiles method next.

Uploading Files Using Ajax

/**
 * Upload the photos using ajax request.
 *
 * @param formData
 */
function uploadFiles(formData) {
    $.ajax({
        url: '/upload_photos',
        method: 'post',
        data: formData,
        processData: false,
        contentType: false,
        xhr: function () {
            var xhr = new XMLHttpRequest();

            // Add progress event listener to the upload.
            xhr.upload.addEventListener('progress', function (event) {
                var progressBar = $('.progress-bar');

                if (event.lengthComputable) {
                    var percent = (event.loaded / event.total) * 100;
                    progressBar.width(percent + '%');

                    if (percent === 100) {
                        progressBar.removeClass('active');
                    }
                }
            });

            return xhr;
        }
    }).done(handleSuccess).fail(function (xhr, status) {
        alert(status);
    });
}

The uploadFiles function takes in the formData and creates a jQuery ajax request passing the formData as the input data to the ajax request. The request is a post request which is submitted to the /upload_photos endpoint.

It is important to set the processData and the contentType fields to false. View the jQuery ajax docs for more info.

The xhr property of the jQuery ajax is used to create a custom ajax request object. Using this we provide our own ajax request object to which we add a upload event listener to the progress event. Then use this to update the progress components width and show the upload progress. Finally return the custom ajax xhr object that is created.

handleSuccess function is provided to handle the ajax response when the ajax request is successful. If the request fails, an alert of the status is shown to the user.

Next, let’s look at the handleSuccess function.

Handling the Ajax Response

Before looking into the handleSuccess function, let look at the structure of the response that will be expected to be built at the backend. This will be a JSON response with the following structure.

[
  {
    "status":true,
    "filename":"file1.jpg",
    "type":"jpg",
    "publicPath":"uploads/file1.jpg"
  },
  {
    "status":true,
    "filename":"file2.png",
    "type":"png",
    "publicPath":"uploads/file2.png"
  },
  {
    "status":false,
    "filename":"file3.zip",
    "message":"Invalid file type"
  }
]

The response contains an array of objects. Each object represents the uploaded file. If the uploaded file is of valid type, then it has a status of true, the uploaded filename, the file type in type and finally the public path in publicPath.

If the file is invalid, then it will have the response as shown in the last object in the above structure.

/**
 * Handle the upload response data from server and display them.
 *
 * @param data
 */
function handleSuccess(data) {
    if (data.length > 0) {
        var html = '';
        for (var i=0; i < data.length; i++) {
            var img = data[i];

            if (img.status) {
                html += '<div class="col-xs-6 col-md-4"><a href="#" class="thumbnail"><img src="' + img.publicPath + '" alt="' + img.filename  + '"></a></div>';
            } else {
                html += '<div class="col-xs-6 col-md-4"><a href="#" class="thumbnail">Invalid file type - ' + img.filename  + '</a></div>';
            }
        }

        $('#album').html(html);
    } else {
        alert('No images were uploaded.')
    }
}

The handleSuccess function uses those object, loops through them and appends the HTML inside the Album Container component.

Also add a onChange event to the file input to reset the progress bar whenever there is a new file upload is started.

// Set the progress bar to 0 when a file(s) is selected.
$('#photos-input').on('change', function () {
    $('.progress-bar').width('0%');
});

That was our front-end code for handling the file upload and submitting them to the endpoint. Now let’s create the Node script to create the endpoints.

This will be our final app.js code.

public/assets/js/app.js
/**
 * Upload the photos using ajax request.
 *
 * @param formData
 */
function uploadFiles(formData) {
    $.ajax({
        url: '/upload_photos',
        method: 'post',
        data: formData,
        processData: false,
        contentType: false,
        xhr: function () {
            var xhr = new XMLHttpRequest();

            // Add progress event listener to the upload.
            xhr.upload.addEventListener('progress', function (event) {
                var progressBar = $('.progress-bar');

                if (event.lengthComputable) {
                    var percent = (event.loaded / event.total) * 100;
                    progressBar.width(percent + '%');

                    if (percent === 100) {
                        progressBar.removeClass('active');
                    }
                }
            });

            return xhr;
        }
    }).done(handleSuccess).fail(function (xhr, status) {
        alert(status);
    });
}

/**
 * Handle the upload response data from server and display them.
 *
 * @param data
 */
function handleSuccess(data) {
    if (data.length > 0) {
        var html = '';
        for (var i=0; i < data.length; i++) {
            var img = data[i];

            if (img.status) {
                html += '<div class="col-xs-6 col-md-4"><a href="#" class="thumbnail"><img src="' + img.publicPath + '" alt="' + img.filename  + '"></a></div>';
            } else {
                html += '<div class="col-xs-6 col-md-4"><a href="#" class="thumbnail">Invalid file type - ' + img.filename  + '</a></div>';
            }
        }

        $('#album').html(html);
    } else {
        alert('No images were uploaded.')
    }
}

// Set the progress bar to 0 when a file(s) is selected.
$('#photos-input').on('change', function () {
    $('.progress-bar').width('0%');
});

// On form submit, handle the file uploads.
$('#upload-photos').on('submit', function (event) {
    event.preventDefault();

    // Get the files from input, create new FormData.
    var files = $('#photos-input').get(0).files,
        formData = new FormData();

    if (files.length === 0) {
        alert('Select atleast 1 file to upload.');
        return false;
    }

    if (files.length > 3) {
        alert('You can only upload up to 3 files.');
        return false;
    }

    // Append the files to the formData.
    for (var i=0; i < files.length; i++) {
        var file = files[i];
        formData.append('photos[]', file, file.name);
    }

    // Note: We are only appending the file inputs to the FormData.
    uploadFiles(formData);
});

Node.js Server Script

There must be 2 endpoints created, one will be a get request to the page index which will return the index.html that was created before. The second one will accept a post request method in which the uploaded files will be processed.

Packages

First, let’s install the required packages for the application using the Node Package Manager (npm).

npm install express formidable read-chunk file-type --save

Express Setup

Let’s setup the express server.

var express = require('express'),
    path = require('path'),
    fs = require('fs'),
    formidable = require('formidable'),
    readChunk = require('read-chunk'),
    fileType = require('file-type');

var app = express();

app.set('port', (process.env.PORT || 5000));

// Tell express to serve static files from the following directories
app.use(express.static('public'));
app.use('/uploads', express.static('uploads'));

/************

Routes will be defined here.

*************/

app.listen(app.get('port'), function() {
    console.log('Express started at port ' + app.get('port'));
});

Here is a basic skeleton of the express server. The script includes all the required packages needed along with the Node fs and path modules.

The express app is initialised. The port in which the application should run is got from the environment variable or it defaults to 5000.

Then the express middleware is used to server the static files from the public directory and the uploads directory. Visit express middleware to know more.

Finally the app is started using the app.listen method.

Creating the Routes Endpoint

index route - /

The first route to be defined is the index route. This will be the default route that users view once they visit the application. This route will send the index.html file that we created using the response objects res.sendFile method.

/**
 * Index route
 */
app.get('/', function (req, res) {
    res.sendFile(path.join(__dirname, 'views/index.html'));
});

Photo Upload route - /upload_photos

Inside this route a photos array is created which will contain the list of photos objects and a new instance of the formidable.IncomingForm is created and assigned to the form variable.

Then the configure options for formidable is set based on the application needs and a event listener is attached to process the uploaded files and other actions. To view all the options and events triggered look at the formidable documentation.

Let’s first set the options needed. Here the option that formidable must expect multiple files and also specify the default temp directory to which it must save the files to.

// Tells formidable that there will be multiple files sent.
form.multiples = true;
// Upload directory for the images
form.uploadDir = path.join(__dirname, 'tmp_uploads');

Then a listener to one of the triggers that formidable provides is added. It’s the file trigger, which is called whenever a new file is received. It accepts a callback which will receive the name and a file object. Refer the formidable docs for the properties present in the file object.

// Invoked when a file has finished uploading.
form.on('file', function (name, file) {
    // Allow only 3 files to be uploaded.
    if (photos.length === 3) {
        fs.unlink(file.path);
        return true;
    }

    var buffer = null,
        type = null,
        filename = '';

    // Read a chunk of the file.
    buffer = readChunk.sync(file.path, 0, 262);
    // Get the file type using the buffer read using read-chunk
    type = fileType(buffer);

    // Check the file type, must be either png,jpg or jpeg
    if (type !== null && (type.ext === 'png' || type.ext === 'jpg' || type.ext === 'jpeg')) {
        // Assign new file name
        filename = Date.now() + '-' + file.name;

        // Move the file with the new file name
        fs.rename(file.path, path.join(__dirname, 'uploads/' + filename));

        // Add to the list of photos
        photos.push({
            status: true,
            filename: filename,
            type: type.ext,
            publicPath: 'uploads/' + filename
        });
    } else {
        // If invalid file type, remove it and set status to false.
        photos.push({
            status: false,
            filename: file.name,
            message: 'Invalid file type'
        });
        fs.unlink(file.path);
    }
});

Inside the callback function, we do the following.

  • Check if the photos length is 3, since only 3 files are allowed. If there is already 3 photos, then delete the current file.
  • Then using the readChunk we get a chunk of the file, that is enough to identify the file type and pass that buffer to the file-type to get the type of the file.
  • If the type of the file is one of png, jpg or jpeg, then we create a new filename using the Date.now and the file.name and move the file to the uploads directory. Finally that file is added to the photos variable with the properties shown in the structure.
  • If the type of the file is not valid, then we add a false object to the photos variable and remove that file using fs.unlink.

And of-course the callback for error event is also provided to log if any error occurs.

form.on('error', function(err) {
    console.log('Error occurred during processing - ' + err);
});

// Invoked when all the fields have been processed.
form.on('end', function() {
    console.log('All the request fields have been processed.');
});

Finally, the request is processed using the parse method and the photos is sent back as a JSON response.

// Parse the incoming form fields.
form.parse(req, function (err, fields, files) {
    res.status(200).json(photos);
});

Our final app.js looks like this.

var express = require('express'),
    path = require('path'),
    fs = require('fs'),
    formidable = require('formidable'),
    readChunk = require('read-chunk'),
    fileType = require('file-type');

var app = express();

app.set('port', (process.env.PORT || 5000));

// Tell express to serve static files from the following directories
app.use(express.static('public'));
app.use('/uploads', express.static('uploads'));

/**
 * Index route
 */
app.get('/', function (req, res) {
    // Don't bother about this :)
    var filesPath = path.join(__dirname, 'uploads/');
    fs.readdir(filesPath, function (err, files) {
        if (err) {
            console.log(err);
            return;
        }

        files.forEach(function (file) {
            fs.stat(filesPath + file, function (err, stats) {
                if (err) {
                    console.log(err);
                    return;
                }

                var createdAt = Date.parse(stats.ctime),
                    days = Math.round((Date.now() - createdAt) / (1000*60*60*24));

                if (days > 1) {
                    fs.unlink(filesPath + file);
                }
            });
        });
    });

    res.sendFile(path.join(__dirname, 'views/index.html'));
});

/**
 * Upload photos route.
 */
app.post('/upload_photos', function (req, res) {
    var photos = [],
        form = new formidable.IncomingForm();

    // Tells formidable that there will be multiple files sent.
    form.multiples = true;
    // Upload directory for the images
    form.uploadDir = path.join(__dirname, 'tmp_uploads');

    // Invoked when a file has finished uploading.
    form.on('file', function (name, file) {
        // Allow only 3 files to be uploaded.
        if (photos.length === 3) {
            fs.unlink(file.path);
            return true;
        }

        var buffer = null,
            type = null,
            filename = '';

        // Read a chunk of the file.
        buffer = readChunk.sync(file.path, 0, 262);
        // Get the file type using the buffer read using read-chunk
        type = fileType(buffer);

        // Check the file type, must be either png,jpg or jpeg
        if (type !== null && (type.ext === 'png' || type.ext === 'jpg' || type.ext === 'jpeg')) {
            // Assign new file name
            filename = Date.now() + '-' + file.name;

            // Move the file with the new file name
            fs.rename(file.path, path.join(__dirname, 'uploads/' + filename));

            // Add to the list of photos
            photos.push({
                status: true,
                filename: filename,
                type: type.ext,
                publicPath: 'uploads/' + filename
            });
        } else {
            photos.push({
                status: false,
                filename: file.name,
                message: 'Invalid file type'
            });
            fs.unlink(file.path);
        }
    });

    form.on('error', function(err) {
        console.log('Error occurred during processing - ' + err);
    });

    // Invoked when all the fields have been processed.
    form.on('end', function() {
        console.log('All the request fields have been processed.');
    });

    // Parse the incoming form fields.
    form.parse(req, function (err, fields, files) {
        res.status(200).json(photos);
    });
});

app.listen(app.get('port'), function() {
    console.log('Express started at port ' + app.get('port'));
});

Once all the files are in place, fire up the Express server using the node app.js from within the root directory and load up the browser at http://localhost:5000.

Hope you guys found this useful and have fun exploring and learning.