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
- What You’ll Build
- Prepare Your Project: Stack & Structure
- Environment Variables and Storing Secrets
- Set Up MongoDB and Connect With Mongoose
- Sending Emails in Node.js
- Load the User’s Profile Picture From Gravatar Using Virtuals in Mongoose
- Implement a User Profile Editing Screen
- Generate a Username in Mongoose Middleware
- Displaying Seasons and Episodes for TV Shows with Mongoose Relationship Population
- Implementing Pagination for Movies
- Implement a Watchlist
- Create a Full Text Search with MongoDB
- Create a REST API with JSON Endpoints
- Update Mongoose Models for JSON Responses
- API Pagination for TV Shows
- Customize API Endpoints with Query Parameters
- Always Throw and Handle API Validation Errors
- Advanced API Validation With Custom Errors
- Create an API Documentation with Swagger
- Customize Your Swagger API Documentation URL
- Describe Endpoint Details in Your Swagger API Documentation
- 10 Tips on API Testing With Postman
- JWT Authentication in Swagger API Documentation
- API Versioning with Request Headers
- API Login With Username and Password to Generate a JWT
- JWT Authentication and Private API Endpoints
- Refresh Tokens With JWT Authentication
- Create a JWT Utility
- JWT Refresh Token for Multiple Devices
- Check Refresh Token in Authentication Strategy
- Rate Limit Your Refresh Token API Endpoint
- How to Revoke a JWT
- Invalidate JWTs With Blacklists
- JWT Logout (Part 1/2)
- JWT “Immediate” Logout (Part 2/2)
- A Better Place to Invalidate Tokens
- How to Switch the JWT Signing Algorithm
- Roll Your Own Refresh Token Authentication Scheme
- JWT Claims 101
- Use JWT With Asymmetric Signatures (RS256 & Co.)
- Encrypt the JWT Payload (The Simple Way)
- Increase JWT Security Beyond the Signature
- Unsigned JSON Web Tokens (Unsecured JWS)
- JWK and JWKS Overview
- Provide a JWKS API Endpoint
- Create a JWK from a Shared Secret
- JWT Verification via JWKS API Endpoint
- What is JOSE in JWT
- Encrypt a JWT (the JWE Way)
- Authenticate Encrypted JWTs (JWE)
- Encrypted and Signed JWT (Nested JWT)
- Bringing Back JWT Decoding and Authentication
- Bringing Back JWT Claims in the JWT Payload
- Basic Authentication With Username and Password
- Authentication and Remember Me Using Cookies
- How to Set a Default Authentication Strategy
- Define Multiple Authentication Strategies for a Route
- Restrict User Access With Scopes
- Show „Insufficient Scope“ View for Routes With Restricted Access
- Access Restriction With Dynamic and Advanced Scopes
- hapi - How to Fix „unknown authentication strategy“
- Authenticate with GitHub And Remember the Login
- Authenticate with GitLab And Remember the User
- How to Combine Bell With Another Authentication Strategy
- Custom OAuth Bell Strategy to Connect With any Server
- Redirect to Previous Page After Login
- How to Implement a Complete Sign Up Flow With Email and Password
- How to Implement a Complete Login Flow
- Implement a Password-Reset Flow
- Views in hapi 9 (and above)
- How to Render and Reply Views
- How to Reply and Render Pug Views (Using Pug 2.0)
- How to Create a Dynamic Handlebars Layout Template
- Create and Use Handlebars Partial Views
- Create and Use Custom Handlebars Helpers
- Specify a Different Handlebars Layout for a Specific View
- How to Create Jade-Like Layout Blocks in Handlebars
- Use Vue.js Mustache Tags in Handlebars Templates
- How to Use Multiple Handlebars Layouts
- How to Access and Handle Request Payload
- Access Request Headers
- How to Manage Cookies and HTTP States Across Requests
- Detect and Get the Client IP Address
- How to Upload Files
- Quick Access to Logged In User in Route Handlers
- How to Fix “handler method did not return a value, a promise, or throw an error”
- How to Fix “X must return an error, a takeover response, or a continue signal”
- Query Parameter Validation With Joi
- Path Parameter Validation With Joi
- Request Payload Validation With Joi
- Validate Query and Path Parameters, Payload and Headers All at Once on Your Routes
- Validate Request Headers With Joi
- Reply Custom View for Failed Validations
- Handle Failed Validations and Show Errors Details at Inputs
- How to Fix AssertionError, Cannot validate HEAD or GET requests
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 bufferfile
: write the incoming file in a temporary one before doing further workstream
: 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!