Super easy image uploads with Multer and Express

Multer makes file uploads via <form enctype="multipart/form-data"></form> really simple. If you're looking to host user images on your filesystem and are wondering about the best practices involved, here's what works for me.

1. The form

For the purposes of the article, I'm going to have a really simple form that submits a POST request to our endpoint and has a single <input type="file"> field.

form(method="post" action="/img" enctype="multipart/form-data")  
  input(type="file" name="photo")

2. The endpoint

Since I want to show this works with the default Express boilerplate, we're going to put our routing in the default routes/index.js file.

First, we need to include multer.

const multer = require('multer')  
const upload = multer({  
  dest: './public/images/users',
  limits: {
    fileSize: 10000000

I recommend uploading the images to a subfolder of ./public/images, in this case users, because this is the default directory where you'll be storing all your images anyway. Note that the path described in dest is relative to the project directory (i.e. where your app.js is stored). I've also set a limit of 10mb but that's optional.

I've got a simple set up going on with passport for user logins and sequelize as my ORM. Using either, however, is optional -- you can handle image submissions without any logins at all and can use mongoose or some other DB system for storing the data.

Using upload.single('<name of input>') middleware adds req.file to the req object which contains metadata about the file.'/img',  
  (req, res) => {

  // Return user if they're not logged in
  if (!req.isAuthenticated()) {
    return res.redirect('/')

  // Just a safety check
  if (!req.file) {
    return res.redirect('/')

  // Your filename is stored in `req.file.filename`, which then goes
  // to your database

  const newUserData = {
    photo: req.file.filename,

What does this code do?

You should now see files with 16 letter hex filenames being stored to your public/images/users directory. Multer does not add any file extensions as a safety precaution, and we now need to write some simple code to send the files with the appropriate mime-type so they're served appropriately.

3. Serving Files

We're going to use read-chunk and image-type modules to check the actual format of the file and serve it with the correct mime type. Install them with npm --save read-chunk image-type and include them in your routes/index.js as so. We also need path and fs default modules.

const readChunk = require('read-chunk')  
const imageType = require('image-type')  
const path = require('path')  
const fs = require('fs')  

Now here's the clever part -- we're going to detect what file type the image is by reading their 'magic bytes' and allow only png, jpg, or jpeg formats.

We're using Express' route parameters so we get the filename dynamically. Then, we store the UPLOAD_PATH and FILE_PATH for the image in appropriate constants.

Using the read-chunk and image-type modules, we read the first 12 bytes of the file and see what file format do they correspond to. We then check for the file formats we're allowing and serve a default image (that is actually a 1x1 transparent png that I've put in the folder) that is sent over if the file is anything fishy.

Otherwise, we set the appropriate mime type and pipe a stream of file data through res via the fs.createReadStream() method.

router.get('/images/users/:img', (req, res) => {  
  const UPLOAD_PATH = './public/images/users/'
  const FILE_PATH = path.join(UPLOAD_PATH, req.params.img)

  let imageData = imageType(readChunk.sync(FILE_PATH, 0, 12))

  if (!(imageData.ext == 'png' 
    || imageData.ext == 'jpg' 
    || imageData.ext == 'jpeg')) fs.createReadStream(path.join(UPLOAD_PATH, 'default.png')).pipe(res)

  res.set('Content-Type', imageData.mime)

Wrapping up

Express makes image uploads slightly complicated, but once you understand the philosophy behind their approach you can get it going really fast and easily. Multer is a very extensible library, even having modules for memory and Amazon S3 storage, among others. The router.get approach makes our application extremely robust and secure against any attacks that are usually otherwise missed.

Discuss on Twitter