At one time or another when building our Node application we have been faced with uploading a photo (usually from a form) to be used as a profile photo for a user in our app. In addition, we usually have to store the photo in the local filesystem (during development) or even in the cloud for easy access. Since this is a very common task, there are lots of tools available which we can leverage to handle the individual parts of the process.
In this tutorial, we will see how to upload a photo and manipulate it (resize, crop, greyscale, etc) before writing it to storage. We will limit ourselves to storing files in the local filesystem for simplicity.
We will be using the following packages to build our application:
multipart/form-data
requests..env
variables to process.env
.We want to take over the uploaded file stream from Multer and then manipulate the stream buffer (image) however we wish using Jimp, before writing the image to storage (local filesystem). This will require us to create a custom storage engine to use with Multer — which we will be doing in this tutorial.
Here is the end result of what we will be building in this tutorial:
We will begin by creating a new Express app using the Express generator. If you don’t have the Express generator already you will need to install it first by running the following command on your command line terminal:
Once you have the Express generator, you can now run the following commands to create a new Express app and install the dependencies for Express. We will be using ejs
as our view engine:
Next, we will install the remaining dependencies we need for our project:
Before we continue, our app will need some form configuration. We will create a .env
file on our project root directory and add some environment variables. The .env
file should look like the following snippet.
Next, we will load our environment variables into process.env
using dotenv so that we can access them in our app. To do this, we will add the following line to the app.js
file. Ensure you add this line at the point where you are loading the dependencies. It must come before all route imports and before creating the Express app instance.
Now we can access our environment variables using process.env
. For example: process.env.AVATAR_STORAGE
should contain the value uploads/avatars
. We will go ahead to edit our index route file routes/index.js
to add some local variables we will be needing in our view. We will add two local variables:
Upload Avatar
process.env.AVATAR_FIELD
Modify the GET /
route as follows:
Let’s begin by creating the basic markup for our photo upload form by modifying the views/index.ejs
file. For the sake of simplicity we will add the styles directly on our view just to give it a slightly nice look. See the following code for the markup of our page.
Notice how we have used our local variables on our view to set the title and the name of the avatar input field. You will notice that we are using enctype="multipart/form-data"
on our form since we will be uploading a file. You will also see that we have set the form to make a POST
request to the /upload
route (we will implement later) on submission.
Now let’s start the app for the first time using npm start
.
If you have been following correctly everything should run without errors. Just visit localhost:3000
on your browser. The page should look like the following screenshot:
So far, trying to upload a photo through our form will result in an error because we’ve not created the handler for the upload request. We are going to implement the /upload
route to actually handle the upload and we will be using the Multer package for that. If you are not already familiar with the Multer package you can check the Multer package on Github.
We will have to create a custom storage engine to use with Multer. Let’s create a new folder in our project root named helpers
and create a new file AvatarStorage.js
inside it for our custom storage engine. The file should contain the following blueprint code snippet:
Let’s begin to add the implementations for the listed functions in our storage engine. We will begin with the constructor function.
Here, we defined our constructor function to accept a couple of options. We also added some default (fallback) values for these options in case they are not provided or they are invalid. You can tweak this to contain more options depending on what you want, but for this tutorial we will stick with the following options for our storage engine.
'local'
for local filesystem. Defaults to 'local'
. You can implement other storage filesystems (like Amazon S3
) if you wish.'jpg'
or 'png'
. Defaults to 'png'
.true
, the output image will be greyscale. Defaults to false
.70
.true
, the image will be cropped to a square. Defaults to false
.px
) of the output image. The default value is 500
. If the smallest dimension of the image exceeds this number, the image is resized so that the smallest dimension is equal to the threshold.true
, three output images of different sizes (lg
, md
and sm
) will be created and stored in their respective folders. Defaults to false
.Let’s implement the methods for creating the random filenames and the output stream for writing to the files:
Here, we use crypto to create a random md5 hash to use as filename and appended the output from the options as the file extension. We also defined our helper method to create writable stream from the given filepath and then return the stream. Notice that a callback function is required, since we are using it on the stream event handlers.
Next we will implement the _processImage()
method that does the actual image processing. Here is the implementation:
A lot is going on in this method but here is a summary of what it is doing:
lg
, md
and sm
) and then an output stream is created using the _createOutputStream()
method for each image file of the respective sizes. The filename for each size takes the format [random_filename_hash]_[size].[output_extension]
. Then the image clone and the stream are put in a batch for processing.Now we will implement the remaining methods and we will be done with our storage engine.
Our storage engine is now ready for use with Multer.
POST /upload
RouteBefore we define the route, we will need to setup Multer for use in our route. Let’s go ahead to edit the routes/index.js
file to add the following:
Here, we are enabling square cropping, responsive images and setting the threshold for our storage engine. We also add limits to our Multer configuration to ensure that the maximum file size is 1 MB
and to ensure that non-image files are not uploaded.
Now let’s add the POST /upload
route as follows:
Notice how we passed the Multer upload middleware before our route handler. The single()
method allows us to upload only one file that will be stored in req.file
. It takes as first parameter, the name of the file input field which we access from process.env.AVATAR_FIELD
.
Now let’s start the app again using npm start
.
visit localhost:3000
on your browser and try to upload a photo. Here is a sample screenshot I got from testing the upload route on Postman using our current configuration options:
You can tweak the configuration options of the storage engine in our Multer setup to get different results.
In this tutorial, we have been able to create a custom storage engine for use with Multer which manipulates uploaded images using Jimp and then writes them to storage. For a complete code sample of this tutorial, checkout the advanced-multer-node-sourcecode repository on Github.
Thanks for learning with the DigitalOcean Community. Check out our offerings for compute, storage, networking, and managed databases.
While we believe that this content benefits our community, we have not yet thoroughly reviewed it. If you have any suggestions for improvements, please let us know by clicking the “report an issue“ button at the bottom of the tutorial.
This textbox defaults to using Markdown to format your answer.
You can type !ref in this text area to quickly search our full set of tutorials, documentation & marketplace offerings and insert the link!
There is an error in the example
Must use modulus operator.