JavaScript
Article

How to Build a File Upload Form with Express and Dropzone.js

By Lukas White

This article was peer reviewed by Panayiotis Velisarakos, Taulant Spahiu and Nilson Jacques. Thanks to all of SitePoint’s peer reviewers for making SitePoint content the best it can be!

Let’s face it, nobody likes forms. Developers don’t like building them, designers don’t particularly enjoy styling them and users certainly don’t like filling them in.

Of all the components that can make up a form, the file control could just be the most frustrating of the lot. A real pain to style, clunky and awkward to use and uploading a file will slow down the submission process of any form.

That’s why a plugin to enhance them is always worth a look, and DropzoneJS is just one such option. It will make your file upload controls look better, make them more user-friendly and by using AJAX to upload the file in the background, at the very least make the process seem quicker. It also makes it easier to validate files before they even reach your server, providing near-instananeous feedback to the user.

We’re going to take a look at DropzoneJS in some detail; show how to implement it and look at some of the ways in which it can be tweaked and customized. We’ll also implement a simple server-side upload mechanism using Node.js.

As ever, you can find the code for this tutorial on our GitHub repository.

Introducing DropzoneJS

As the name implies, DropzoneJS allows users to upload files using drag n’ drop. Whilst the usability benefits could justifiably be debated, it’s an increasingly common approach and one which is in tune with the way a lot of people work with files on their desktop. It’s also pretty well supported across major browsers.

DropzoneJS isn’t simply a drag n’drop based widget, however; clicking the widget launches the more conventional file chooser dialog approach.

Here’s an animation of the widget in action:

The DropzoneJS widget in action

Alternatively, take a look at this, most minimal of examples.

You can use DropzoneJS for any type of file, though the nice little thumbnail effect makes it ideally suited to uploading images in particular.

Features

To summarize some of the plugin’s features and characteristics:

  • Can be used with or without jQuery
  • Drag and drop support
  • Generates thumbnail images
  • Supports multiple uploads, optionally in parallel
  • Includes a progress bar
  • Fully themeable
  • Extensible file validation support
  • Available as an AMD module or RequireJS module
  • It comes in at around 33Kb when minified

Browser Support

Taken from the official documentation, browser support is as follows:

  • Chrome 7+
  • Firefox 4+
  • IE 10+
  • Opera 12+ (Version 12 for MacOS is disabled because their API is buggy)
  • Safari 6+

There are a couple of ways to handle fallbacks for when the plugin isn’t fully supported, which we’ll look at later.

Installation

The simplest way to install DropzoneJS is via Bower:

bower install dropzone

Alternatively you can grab it from Github, or simply download the standalone JavaScript file — though bear in mind you’ll also need the basic styles, also available in the Github repo.

There are also third-party packages providing support for ReactJS and implementing the widget as an Angular directive.

First Steps

If you’ve used the Bower or download method, make sure you include both the main JS file and the styles (or include them into your application’s stylesheet), e.g:

<link rel="stylesheet" href="/path/to/dropzone.css">
<script type="text/javascript" src="/path/to/dropzone.js"></script>

Pre-minified versions are also supplied in the package.

If you’re using RequireJS, use dropzone-amd-module.js instead.

Basic Usage

The simplest way to implement the plugin is to attach it to a form, although you can use any HTML such as a div tag. Using a form, however, means less options to set — most notably the URL, which is the most important configuration property.

You can initialize it simply by adding the dropzone class, for example:

<form id="upload-widget" method="post" action="/upload" class="dropzone">
</form>

Technically that’s all you need to do, though in most cases you’ll want to set some additional options. The format for that is as follows:

Dropzone.options.WIDGET_ID = {
  //
};

To derive the widget ID for setting the options, take the ID you defined in your HTML and camel-case it. For example, upload-widget becomes uploadWidget:

Dropzone.options.uploadWidget = {
  //
};

You can also create an instance programmatically:

var uploader = new Dropzone(‘#upload-widget’, options);

Next up, we’ll look at some of the available configuration options.

Basic Configuration Options

The url option defines the target for the upload form, and is the only required parameter. That said, if you’re attaching it to a form element then it’ll simply use the form’s action attribute, in which case you don’t even need to specify that.

The method option sets the HTTP method and again, it will take this from the form element if you use that approach, or else it’ll simply default to POST, which should suit most scenarios.

The paramName option is used to set the name of the parameter for the uploaded file; were you using a file upload form element, it would match the name attribute. If you don’t include it then it defaults to file.

maxFiles sets the maximum number of files a user can upload, if it’s not set to null.

By default the widget will show a file dialog when it’s clicked, though you can use the clicked parameter to disable this by setting it to false, or alternatively you can provide an HTML element or CSS selector to customize the clickable element.

Those are the basic options, but let’s now look at some of the more advanced options.

Enforcing Maximum File Size

The maxFilesize property determines the maximum file size in megabytes. This defaults to a size of 1000 bytes, but using the filesizeBase property, you could set it to another value — for example, 1024 bytes. You may need to tweak this to ensure that your client and server code calculate any limits in precisely the same way.

Restricting to Certain File Types

The acceptFiles parameter can be used to restrict the type of file you want to accept. This should be in the form of a comma-separated list of MIME-types, although you can also use wildcards.

For example, to only accept images:

acceptedFiles: ‘image/*’,

Modifying the Size of the Thumbnail

By default, the thumbnail is generated at 120px by 120px; i.e., it’s square. There are a couple of ways you can modify this behavior.

The first is to use the thumbnailWidth and/or the thumbnailHeight configuration options.

If you set both thumbnailWidth and thumbnailHeight to null, the thumbnail won’t be resized at all.

If you want to completely customize the thumbnail generation behavior, you can even override the resize function.

One important point about modifying the size of the thumbnail; the dz-image class provided by the package sets the thumbnail size in the CSS, so you’ll need to modify that accordingly as well.

Additional File Checks

The accept option allows you to provide additional checks to determine whether a file is valid, before it gets uploaded. You shouldn’t use this to check the number of files (maxFiles), file type (acceptedFiles), or file size (maxFilesize), but you can write custom code to perform other sorts of validation.

You’d use the accept option like this:

accept: function(file, done) {
  if ( !someCheck() ) {
    return done('This is invalid!');
  }
  return done();
}

As you can see it’s asynchronous; call done() with no arguments and validation passes, or provide an error message and the file will be rejected, displaying the message alongside the file as a popover.

We’ll look at a more complex, real-world example later in the article, when we’ll look at how to enforce minimum or maximum image sizes.

Sending Additional Headers

Often you’ll need to attach additional headers to the uploader’s HTTP request.

As an example, one approach to CSRF (Cross Site Request Forgery) protection is to output a token in the view, then have your POST/PUT/DELETE endpoints check the request headers for a valid token. Suppose you outputted your token like this:

<meta name="csrf-token" content="CL2tR2J4UHZXcR9BjRtSYOKzSmL8U1zTc7T8d6Jz">

Then, you could add this to the configuration:

headers: {
  'x-csrf-token': document.querySelectorAll('meta[name=csrf-token]')[0].getAttributeNode('content').value,
},

Alternatively, here’s the same example but using jQuery:

headers: {
  'x-csrf-token': $('meta[name="csrf-token"]').attr('content')
},

Your server should then verify the x-csrf-token header, perhaps using some middleware.

Handling Fallbacks

The simplest way to implement a fallback is to insert a <div> into your form containing input controls, setting the class name on the element to fallback. For example:

<form id="upload-widget" method="post" action="/upload" class="dropzone">
  <div class="fallback">
    <input name="file" type="file" />
  </div>
</form>

Alternatively, you can provide a function to be executed when the browser doesn’t support the plugin using the fallback configuration parameter.

You can force the widget to use the fallback behavior by setting forceFallback to true, which might help during development.

Handling Errors

You can customize the way the widget handles errors by providing a custom function using the error configuration parameter. The first argument is the file, the error message the second and if the error occured server-side, the third parameter will be an instance of XMLHttpRequest.

As always, client-side validation is only half the battle. You must also perform validation on the server. When we implement a simple server-side component later we’ll look at the expected format of the error response, which when properly configured will be displayed in the same way as client-side errors (illustrated below).

Displaying errors with DropzoneJS

Overriding Messages and Translation

There are a number of additional configuration properties which set the various messages displayed by the widget. You can use these to customize the displayed text, or to translate them into another language.

Most notably, dictDefaultMessage is used to set the text which appears in the middle of the dropzone, prior to someone selecting a file to upload.

You’ll find a complete list of the configurable string values – all of which begin with dictin the documentation.

Events

There are a number of events you can listen to in order to customize or enhance the plugin.

There are two ways to listen to an event. The first is to create a listener within an initialization function:

Dropzone.options.uploadWidget = {
  init: function() {
    this.on('success', function( file, resp ){
      ...
    });
  },
  ...
};

Or the alternative approach, which is useful if you decide to create the Dropzone instance programatically:

var uploader = new Dropzone('#upload-widget');
uploader.on('success', function( file, resp ){
  ...
});

Perhaps the most notable is the success event, which is fired when a file has been successfully uploaded. The success callback takes two arguments; the first, a file object and the second an instance of XMLHttpRequest.

Other useful events include addedfile and removedfile for when a file has been added or removed from the upload list, thumbnail which fires once the thumbnail has been generated and uploadprogress which you might use to implement your own progress meter.

There are also a bunch of events which take an event object as a parameter and which you could use to customize the behavior of the widget itself – drop, dragstart, dragend, dragenter, dragover and dragleave.

You’ll find a complete list of events in the relevant section of the documentation.

A More Complex Validation Example: Image Dimensions

Earlier we looked at the asynchronous accept() option, which you can use to run checks (validation) on files before they get uploaded.

A common requirement when you’re uploading images is to enforce minimum or maximum image dimensions. We can do this with DropzoneJS, although it’s slightly more complex.

Although the accept callback receives a file object, in order to check the image dimensions we need to wait until the thumbnail has been generated, at which point the dimensions will have been set on the file object. To do so, we need to listen to the thumbnail event.

Here’s the code; in this example we’re checking that the image is at least 640 x 480px before we upload it:

init: function() {
  this.on('thumbnail', function(file) {
    if ( file.width < 1024 || file.height < 768 ) {
      file.rejectDimensions();
    }
    else {
      file.acceptDimensions();
    }
  });
},
accept: function(file, done) {
  file.acceptDimensions = done;
  file.rejectDimensions = function() {
    done('The image must be at least 1024 by 768 pixels in size');
  };
},

A Complete Example

Having gone through the options, events and some slightly more advanced validation, let’s look at a complete and relatively comprehensive example. Obviously we’re not taking advantage of every available configuration option, since there are so many — making it incredibly flexible.

Here’s the HTML for the form:

<form id="upload-widget" method="post" action="/upload" class="dropzone">
  <div class="fallback">
    <input name="file" type="file" />
  </div>
</form>

If you’re implementing CSRF protection, you may want to add something like this to your layouts:

<head>
  <!-- -->
  <meta name="csrf-token" content="XYZ123">
</head>

Now the JavaScript – notice we’re not using jQuery!

Dropzone.options.uploadWidget = {
  paramName: 'file',
  maxFilesize: 2, // MB
  maxFiles: 1,
  dictDefaultMessage: 'Drag an image here to upload, or click to select one',
  headers: {
    'x-csrf-token': document.querySelectorAll('meta[name=csrf-token]')[0].getAttributeNode('content').value,
  },
  acceptedFiles: 'image/*',
  init: function() {
    this.on('success', function( file, resp ){
      console.log( file );
      console.log( resp );
    });
    this.on('thumbnail', function(file) {
      if ( file.width < 640 || file.height < 480 ) {
        file.rejectDimensions();
      }
      else {
        file.acceptDimensions();
      }
    });
  },
  accept: function(file, done) {
    file.acceptDimensions = done;
    file.rejectDimensions = function() {
      done('The image must be at least 640 x 480px')
    };
  }
};

A reminder that you’ll find the code for this example on our GitHub repository.

Hopefully, this is enough to get you started for most scenarios; check out the full documentation if you need anything more complex.

Theming

There are a number of ways to customize the look and feel of the widget, and indeed it’s possible to completely transform the way it looks.

For an example of just how customizable the appearance is, here is a demo of the widget tweaked to look and feel exactly like the jQuery File Upload widget using Bootstrap.

Obviously the simplest way to change the widget’s appearance is to use CSS. The widget has a class of dropzone and its component elements have classes prefixed with dz-; for example dz-clickable for the clickable area inside the dropzone, dz-message for the caption, dz-preview / dz-image-preview for wrapping the previews of each of the uploaded files, and so on. Take a look at the dropzone.css file for reference.

You may also wish to apply styles to the hover state; that is, when the user hovers a file over the dropzone before releasing their mouse button to initiate the upload. You can do this by styling the dz-drag-hover class, which gets added automatically by the plugin.

Beyond CSS tweaks, you can also customize the HTML which makes up the previews by setting the previewTemplate configuration property. Here’s what the default preview template looks like:

<div class="dz-preview dz-file-preview">
  <div class="dz-image">
    <img data-dz-thumbnail />
  </div>
  <div class="dz-details">
    <div class="dz-size">
      <span data-dz-size></span>
    </div>
    <div class="dz-filename">
      <span data-dz-name></span>
    </div>
  </div>
  <div class="dz-progress">
    <span class="dz-upload" data-dz-uploadprogress></span>
  </div>
  <div class="dz-error-message">
    <span data-dz-errormessage></span>
  </div>
  <div class="dz-success-mark">
    <svg>REMOVED FOR BREVITY</svg>
  </div>
  <div class="dz-error-mark">
    <svg>REMOVED FOR BREVITY</svg>
  </div>
</div>

As you can see, you get complete control over how files are rendered once they’ve been queued for upload, as well as success and failure states.

That concludes the section on using the DropzoneJS plugin. To round up, let’s look at how to get it working with server-side code.

A Simple Server-Side Upload Handler with Node.js and Express

Naturally you can use any server-side technology for handling uploaded files. In order to demonstrate how to integrate your server with the plugin, we’ll build a very simple example using Node.js and Express.

To handle the uploaded file itself we’ll use multer, a package which provides some Express middleware that makes it really easy. In fact, this easy:

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

app.post( '/upload', upload.single( 'file' ), function( req, res, next ) {
  // Metadata about the uploaded file can now be found in req.file
});

Before we continue the implementation, the most obvious question to ask when dealing with a plugin like DropzoneJS which makes requests for you behind the scenes is: “what sort of responses does it expect?”

Handling Upload Success

If the upload process is successful, the only requirement as far as your server-side code is concerned, is to return a 2xx response code. The content and format of your response is entirely up to you, and will probably depend on how you’re using it; for example you might return a JSON object which contains a path to the uploaded file, or the path to an automatically generated thumbnail. For the purposes of this example we’ll simply return the contents of the file object – i.e. a bunch of metadata – provided by Multer:

return res.status( 200 ).send( req.file );

The response will look something like this:

{ fieldname: 'file',
  originalname: 'myfile.jpg',
  encoding: '7bit',
  mimetype: 'image/jpeg',
  destination: 'uploads/',
  filename: 'fbcc2ddbb0dd11858427d7f0bb2273f5',
  path: 'uploads/fbcc2ddbb0dd11858427d7f0bb2273f5',
  size: 15458 }

Handling Upload Errors

If your response is in JSON format – that is to say, your response type is set to application/json – then DropzoneJS default error plugin expects the response to look like this:

{
  error: 'The error message'
}

If you aren’t using JSON, it’ll simply use the response body, for example:

return res.status( 422 ).send( 'The error message' );

Let’s demonstrate this by performing a couple of validation checks on the uploaded file. We’ll simply duplicate the checks we performed on the client — remember, client-side validation is never sufficient on its own.

To verify that the file is an image, we’ll simply check that the Mime-type starts with image/. ES6’s String.prototype.startsWith() is ideal for this, but let’s install a polyfill for it:

npm install string.prototype.startswith --save

Here’s how we might run that check and, if it fails, return the error in the format which Dropzone’s default error handler expects:

if ( !req.file.mimetype.startsWith( 'image/' ) ) {
  return res.status( 422 ).json( {
    error : 'The uploaded file must be an image'
  } );
}

I’m using HTTP Status Code 422, Unprocessable Entity here for validation failure, but 400 Bad Request is just as valid; indeed anything outside of the 2xx range will cause the plugin to report the error.

Let’s also check that the image is of a certain size; the image-size package makes it really straightforward to get the dimensions of an image. You can use it asynchronously or synchronously; we’ll use the latter to keep things simple:


var dimensions = sizeOf( req.file.path );

if ( ( dimensions.width < 640 ) || ( dimensions.height < 480 ) ) {
  return res.status( 422 ).json( {
    error : 'The image must be at least 640 x 480px'
  } );
}

Let’s put all of it together in a complete (mini) application:

var express  =  require( 'express' );
var multer   =  require( 'multer' );
var upload   =  multer( { dest: 'uploads/' } );
var sizeOf   =  require( 'image-size' );
var exphbs   =  require( 'express-handlebars' );
require( 'string.prototype.startswith' );

var app = express();

app.use( express.static( __dirname + '/bower_components' ) );

app.engine( '.hbs', exphbs( { extname: '.hbs' } ) );
app.set('view engine', '.hbs');

app.get( '/', function( req, res, next ){
  return res.render( 'index' );
});

app.post( '/upload', upload.single( 'file' ), function( req, res, next ) {

  if ( !req.file.mimetype.startsWith( 'image/' ) ) {
    return res.status( 422 ).json( {
      error : 'The uploaded file must be an image'
    } );
  }

  var dimensions = sizeOf( req.file.path );

  if ( ( dimensions.width < 640 ) || ( dimensions.height < 480 ) ) {
    return res.status( 422 ).json( {
      error : 'The image must be at least 640 x 480px'
    } );
  }

  return res.status( 200 ).send( req.file );
});

app.listen( 8080, function() {
  console.log( 'Express server listening on port 8080' );
});

For brevity, this server-side code doesn’t implement CSRF protection; you might want to look at a package like CSURF for that.

You’ll find this code, along with the supporting assets such as the view, in the accompanying repository.

Summary

DropzoneJS is a slick, powerful and highly customizable JavaScript plugin for super-charging your file upload controls and performing AJAX uploads. In this article we’ve taken a look at a number of the available options, at events and how to go about customizing the plugin. There’s a lot more to it than can reasonably be covered in one article, so check out the official website if you’d like to know more — but hopefully this is enough to get you started.

We’ve also built a really simple server-side component to handle file uploads, demonstrating how to get the two working in tandem.

  • PVgr

    Excellent article Lucas, thorough and well written. Thanks for sharing!

  • http://www.adriansandu.com Adrian SANDU

    While some of the technical details are a bit beyond my knowledge (and the blame falls on me not on the author), the entire article is a well written recipe. Follow the steps and you will get the result. Good job!

  • eddie404

    Nice article Lukas. Thanks.

  • http://www.givenm.co.za/ Given M

    Brilliant! Thanks for this :)

Recommended

Learn Coding Online
Learn Web Development

Start learning web development and design for free with SitePoint Premium!

Get the latest in JavaScript, once a week, for free.