Simple image upload with Express

Recently I have been working on a project that involved uploading images as a profile image for users. So I thought that I will share a simple way of uploading images in Express.

The app is available on Github - please follow the setup instruction to run it on your system.

Let’s take a simple flow for uploading an image for a user’s profile:

  • User selects an image from the local disk.
  • Image gets uploaded through a form.
  • The application analyzes the data from the form and validates it. The image is uploaded to a temporary folder
  • We move the image from a temporary folder to a permanent path.
  • Respond with a success message to the user.

First, let’s discuss the front-end component - for this I will use EJS to create my form’s markup. We will create a simple upload form, nothing fancy.

<form action="/upload" method="post" enctype="multipart/form-data">  
  <label for="file">Select your image:</label>
  <input type="file" name="file" id="file" />
  <span class="hint">Supported files: jpg, jpeg, png.</span>
  <button type="submit">upload</button>
</form>

Now let’s get started with the application logic, we will start by defining our package.json file. You should add to your package.json the following dependencies:

"dependencies": {
    "express": "latest",
    "ejs": "latest",
    "uid2": "latest",
    "mime": "latest"
}

We are using Express as our web application framework, the default view engine will be EJS, in other words we are going to use EJS to create our views. I have used uid2 to create random names for the uploaded files. To determine the file type we can use mime, a practical little helper library.

Next step will be to create the app.js file, a starting point for the application, in larger applications a tend to name this file server.js and separate the app functionalities. I usually put my dependency and variable definition on top:

//Define variables & dependencies

var express = require('express');  
var app = express();  
var server = require('http').createServer(app);  
var controllers = require('./controllers');

Next I will configure Express to meet my requirements, first I will set the default engine as stated before we are using ejs, so we are going to compile the html files. Second, we are going to define a default place for the views. And last, we are going to tell Express that our view files are going to have the default extension .html.

//Configure Express

app.engine('.html', require('ejs').__express);  
app.set('views', __dirname + '/views');  
// Without this you would need to
// supply the extension to res.render()
// ex: res.render('users.html')
app.set('view engine', 'html');  

We are going to set up a pretty default configuration:

app.configure(function() {  
  app.use(express.bodyParser());
  app.use(express.methodOverride());
  app.use(express.static(__dirname + '/public'));
  app.use('/images', express.static(__dirname + '/writable'));
  app.use(app.router);
  app.engine('html', require('ejs').renderFile);
});

One thing to mention here is that I have used to static folders, one is /public where you can store your css and js files, and another one /images which will serve as our final path of the uploaded images. As you can see I am linking the /images route to a static folder /writable.

In order to access the upload functionality we are going to create two routes:

//Define routes
app.get('/', controllers.index);  
app.post('/upload', controllers.upload);  

The first / is going to serve us as our index route which will respond only to a GET request, and the second will be a POST on /upload path. As you can see I have separated the application logic into another file.

Finally we are going to start our server:

//Start the server
server.listen(3000, function() {  
  console.log("Express server up and running.");
});

The whole content of the file can be found here.

Initially I wanted to write all the application logic into a single file, but instead I separated it. I have created a new file called controller.js, first as usual I am going to declare my dependencies:

//Dependencies
var fs = require('fs');  
var path = require('path');  
var uid = require('uid2');  
var mime = require('mime');  

Also a good practice in order to define constants is to write them with capital letters to reflect their state so that their value’s should not be changed:

//Constants
var TARGET_PATH = path.resolve(__dirname, '../writable/');  
var IMAGE_TYPES = ['image/jpeg', 'image/png'];  

The TARGET_PATH is going to hold the final path of the uploaded images. In order to add some validation to file types, I have created an Array with accepted file types, in our case we are supporting only images.

To access the functionalities from controller.js file we need to export it, so that other files can use them. On easy way is to attach to the modules.exports ( or exports a shorter version ) what we want to export, in our case we are going to export a whole object:

module.exports = {  
  index: function(req, res, next) {
    // some logic
  },
  upload: function(req, res, next) {
    // some logic
  }
};

The index method is going to respond with the index.html file, so we are going to change it to something similar to this:

index: function(req, res, next) {  
    res.render('index');
},

Now the upload method is going to be a little bit more complex, as it has to deal with the upload functionality. You can access the uploaded files in the req variable’s files property in Express.

upload: function(req, res, next) {  
    var is;
    var os;
    var targetPath;
    var targetName;
    var tempPath = req.files.file.path;
    //get the mime type of the file
    var type = mime.lookup(req.files.file.path);
    //get the extension of the file
    var extension = req.files.file.path.split(/[. ]+/).pop();

    //check to see if we support the file type
    if (IMAGE_TYPES.indexOf(type) == -1) {
      return res.send(415, 'Supported image formats: jpeg, jpg, jpe, png.');
    }

    //create a new name for the image
    targetName = uid(22) + '.' + extension;

    //determine the new path to save the image
    targetPath = path.join(TARGET_PATH, targetName);

    //create a read stream in order to read the file
    is = fs.createReadStream(tempPath);

    //create a write stream in order to write the a new file
    os = fs.createWriteStream(targetPath);

    is.pipe(os);

    //handle error
    is.on('error', function() {
      if (err) {
        return res.send(500, 'Something went wrong');
      }
    });

    //if we are done moving the file
    is.on('end', function() {

      //delete file from temp folder
      fs.unlink(tempPath, function(err) {
        if (err) {
          return res.send(500, 'Something went wrong');
        }

        //send something nice to user
        res.render('image', {
          name: targetName,
          type: type,
          extension: extension
        });

      });//#end - unlink
    });//#end - on.end
  }
Breaking down the method:

We need to get the type of the file in order to validate it, we also need to store the extension of the file, later we will use the extension when renaming the file.

//get the mime type of the file
var type = mime.lookup(req.files.file.path);  
//get the extenstion of the file
var extension = req.files.file.path.split(/[. ]+/).pop();

//check to see if we support the file type
if (IMAGE_TYPES.indexOf(type) == -1) {  
    return res.send(415, 'Supported image formats: jpeg, jpg, jpe, png.');
}

Create a new name for the file, remember we added uid2 to dependencies, we are going to use the uid() function in order to create a new random name for our file and concatenate the extracted extension.

targetName = uid(22) + '.' + extension;  

There are a few approaches regarding the moving of the file from a temporary place to a permanent place. One of the is to use fs.rename():

fs.rename(tmpPath, targetPath, function(err) {  
    if (err) throw err;
    console.log('Done.');
});

This is pretty simple but, normally /tmp is in tmpfs, in ram and you cannot directly rename two files from different devices, it gives EXDEV error. Instead of using this method, I have created a read and write stream in order to accomplish the copy to a different path functionality.

//determine the new path to save the image
targetPath = path.join(TARGET_PATH, targetName);

//create a read stream in order to read the file
is = fs.createReadStream(tempPath);

//create a write stream in order to write the a new file
os = fs.createWriteStream(targetPath);

is.pipe(os);  

When the stream is done it will dispatch an end event to which we can attach our own callback:

//if we are done moving the file
is.on('end', function() {

    //delete file from temp folder
    fs.unlink(tempPath, function(err) {
        if (err) {
        return res.send(500, 'Something went wrong');
        }

        //send something nice to user
        res.render('image', {
            name: targetName,
            type: type,
            extension: extension
        });

    });//#end - unlink
});//#end - on.end

A good practice is to delete your temporary uploaded file after you moved it, for this we are going to use fs.unlink(), if everything goes well we can send a message to the user that the file has been successfully uploaded.

There is a lot more work that can be done here - especially when it comes to event handling and validation. A future improvement that I would like to add is to show a nice progress of the uploading to the user.

Thank you for reading. I hope you have found some useful information in this article. Until next time.

You can leave comments on hackernews.

comments powered by Disqus