hapi — How to Upload Files

Web application development with hapi allows you to build with rapid speed. Many boilerplates exist and provide the fundamental structure with functionality for user handling, view template management and more. That means you can jumpstart right into the nitty gritty development of your application.

And of course in these social times, you need the handling of files in your app! Every user on your platform wants to upload their preferred profile picture or any kind of file based resources.

This tutorial shows you how to handle file uploads with hapi.

hapi Series Overview

Upload File and Additional Data

Usually, you’re not just sending a file and that’s it. You want to allow additional data besides the file within the request payload. That’s totally fine, hapi got your back and allows that without any problems.

Let’s take the description from the previous paragraph and allow profile updates including a new avatar for the user. What you want is a (POST or PUT) route for profile updates. Also, you want to define how payload will be handled for the route, especially requests including a file. Take a look at the following code snippet and you’ll get further descriptions below.

hapi v17

server.route({  
  method: 'PUT',
  path: '/profile',
  config: {
    handler: (request, h) => {
      const payload = request.payload

      console.log(payload)

      return 'Received your data'
    },
    payload: {
      maxBytes: 209715200,
      output: 'file',
      parse: true
    }
  }
})

hapi v16

server.route({  
  method: 'PUT',
  path: '/profile',
  config: {
    handler: function (request, reply) {
      const payload = request.payload

      console.log(payload)

      reply('Received your data')
    },
    payload: {
      maxBytes: 209715200,
      output: 'file',
      parse: true
    }
  }
})

The route’s handler simply accesses and logs the incoming data within the request’s payload and replies with “Received your data“. More interesting is the config.payload object, containing the maxBytes, output and parse properties.

With maxBytes you define the maximum payload size in bytes that your hapi server allows to handle and keep in memory. This value defaults to 1048576, which is 1 MB.

The output controls whether you keep the file in memory, a temporary file or receive the file as a stream:

  • data: the complete file is in memory as a buffer
  • file: write the incoming file in a temporary one before doing further work
  • stream: file is available as a stream

The parse property determines if the incoming payload gets parsed (true, default) or presented raw (false).

Assuming that we do an exemplary PUT request to the route from above with values for username and avatar, you’d expect the following console output.

{ username: 'marcus',
  avatar: { 
    filename: 'marcus_avatar.jpg',
    path: '/var/folders/cq/143/T/146-20-dab',
    headers: { 
      'content-disposition': 'form-data; name="avatar"; filename="marcus_avatar.jpg"',
      'content-type': 'image/jpeg'
    },
    bytes: 82521
  }
}

You can see that there are two properties within the request payload: username and avatar. The chosen file output for the avatar writes the picture in a temporary file first and you can now do further processing using the file’s path and original filename.

Depending on your use case, you might want to copy and rename the file to a specific location or upload to a cloud storage like Amazon S3.

We’ve mentioned the data value for config.payload.output that keeps the complete file in memory, available as a buffer:

hapi v17

server.route({  
  method: 'POST',
  path: '/profile-data',
  config: {
    handler: (request, reply) => {
      console.log(request.payload)
    },
    payload: {
      output: 'data',
    }
  }
})

hapi v16

server.route({  
  method: 'POST',
  path: '/profile-data',
  config: {
    handler: function (request, reply) {
      console.log(request.payload)
      reply()
    },
    payload: {
      output: 'data',
    }
  }
})

Notice that the value for config.payload.output is set to data which will keep the complete file in memory! This configuration is not suitable for uploads containing large files. If you don’t plan to allow files larger than one megabyte, you can skip the maxBytes configuration. The request payload gets parsed by default.

Sending a sample request to /profile-data and logging the parsed request payload results in this output:

{ username: 'marcus',
  pictures: <Buffer ff d8 ff ... > }

In comparison to the temporary file, you don’t need to read the file again from your server’s file system, but instead make use of its buffer right from memory.

As mentioned above, you want to keep the memory impact low and also don’t store every uploaded file in a temporary one. That’s where you should leverage streams. If you want to go this route, read the next section!

Upload Data and File As Stream

When working with files of large size, you should depend on streams as the upload output. Of course you can choose stream for any file size and not just larger ones. If you want a stream, you get it! The following code snippet outlines the configuration to tell hapi you want a read stream of the uploaded file.

The stream configuration only applies to files, further information within the request payload are available as usual within the parsed object.

hapi v17

server.route({  
  method: 'POST',
  path: '/upload-stream',
  config: {
    handler: (request, h) {
      const payload = request.payload

      console.log(payload)

      return 'I have a stream'
    },
    payload: {
      output: 'stream',
      parse: true
    }
  }
})

hapi v16

server.route({  
  method: 'POST',
  path: '/upload-stream',
  config: {
    handler: function (request, reply) {
      const payload = request.payload

      console.log(payload)

      reply('I have a stream')
    },
    payload: {
      output: 'stream',
      parse: true
    }
  }
})

The route handler at path /upload-stream logs the request payload and replies a simple text „I have a stream“. The interesting part is the config.payload object that specifies the payload handling. You need to define output: 'stream' and hapi provides incoming files as a stream. Take a look at the console log for an exemplary request payload containing a username as string and avatar as JPG:

{ username: 'marcus',
  avatar:
   Readable {
     _readableState:
      ReadableState {
      …
     _encoding: 'utf8',
     hapi:
      { filename: 'marcus_avatar.jpg',
        headers: [Object] } } }

I’ve shortened the output above to show the interesting parts. At first the two fields that came in from client side: username and avatar. And second, please notice the hapi object on avatar that contains the filename and headers properties. The hapi property is only available for file streams from multipart/form-data uploads.

Now that you’ve the avatar available as a stream, you can use it as simply as:

payload.avatar.pipe(anotherStream)  

Pipe the data into another stream and do your desired processing.

Up to this point, you’ve learned how to handle file uploads where a single file is either available in memory, in a temporary file or as a stream. If you want to upload multiple files simultaneously, the next section is for you!

Upload Multiple Files

At this point, you already know how to handle a single incoming file within the request payload using hapi. There are already use cases in your head that would require the option to upload multiple files from a client. And of course hapi got you covered for this situation.

Actually, there’s no difference in the route configuration for a single or multiple files. The only difference is the property that hapi will provide the request payload. When sending multiple files using the same key, hapi will provide a list that contains the individual files.

Depending on the output defined on your route in config.payload, you’ll either receive the files as buffers, temporary ones or streams. The defined output is applied to all the incoming files.

The following parsed object illustrates a sample request containing the username property and three pictures where each picture is stored in a temporary file:

{ username: 'marcus',
  pictures:
   [ { filename: 'Bildschirmfoto 2015-12-18 um 15.11.50.png',
       path: '/var/folders/rq/q_m4_21j3lqf1lw48fqttx_80000gn/T/1487606162810-29302-4f33a879f7284deb',
       headers: [Object],
       bytes: 127331 },
     { filename: 'Bildschirmfoto 2016-01-09 um 16.03.00.png',
       path: '/var/folders/rq/q_m4_21j3lqf1lw48fqttx_80000gn/T/1487606162821-29302-c4441f06b0d68939',
       headers: [Object],
       bytes: 167728 },
     { filename: 'Bildschirmfoto 2015-12-18 um 14.54.43.png',
       path: '/var/folders/rq/q_m4_21j3lqf1lw48fqttx_80000gn/T/1487606162811-29302-40c773ae9e63e73e',
       headers: [Object],
       bytes: 157733 } ] }

The option used in the code snippet above is output: 'file' which makes each individual picture from the pictures array available in a temporary file. In case you’d want each picture as a stream (output: 'stream'), the entries within the pictures array would be Readable streams, ready for further processing. The same holds true for output: 'data' where you’d receive a buffer for each picture.

Outlook

In this tutorial, you’ve learned the details about file uploads in hapi and available output options. Depending on your use case, you need to decide between files completely in memory, a temporary file or as a stream. Choose what fits you best and benefit from the simplicity hapi provides for the file uploads.

We’re happy to hear your thoughts and comments. Please don’t hesitate to use the comments below or find us on Twitter @futurestud_io. Let us know what you think!

Enjoy coding & make it rock!

Explore the Library

Find interesting tutorials and solutions for your problems.