Recently while working on my latest project I wanted to integrate file uploads. In the past I used React, Node & Amazon S3 to do this, but this time around I’m using Vue (w/ Vue CLI 3) & running a Digital Ocean box. Although getting it set up was pretty easy, there were a couple of gotchas that had me frantically googling & searching Stack Overflow / Github — in this article I thought I’d outline the process / code I went through, so hopefully you learn something AND save time ;) By the end of this you’ll have file uploads from a Vue app to Digital Ocean Spaces sorted out.

Tech Stack

Before we start — here’s the main tech I’ve used on the (Ubuntu 18.0.4) Server:

Node 10

Express 4.16.14

Multer 1.4.1 & Multer S3 2.9.0

AWS SDK (JavaScript)

And the Client:

Vue 2.5.21 with Vue CLI 3

Vuetify 1.3.15

Vuex 3.0.1

Axios 0.18.0

Let’s begin on the client side.

I’m going to assume that you already have a Vue project set up, and if you’re not using Vuetify most of the code will still apply, just with slightly different markup. For this article I’ll be using the example of uploading a profile pic / avatar, but it applies for any type of file upload!

To enable people to upload files you’ll want some kind of component, and I chose to open up a modal when users click their current profile pic. Inside that modal I included the current user profile picture, and 3 buttons: Upload, Save & Cancel:

Here I’m using a v-if directive to show the user’s old profile picture until the new one is loaded on the client

You’ll notice that the UploadButton is unlike Cancel & Save — it’s a component called Vuetify Upload Button which you can find here: https://github.com/doritobandito/vuetify-upload-button

I used that component since Vuetify doesn’t currently include a dedicated upload component and it includes some handy defaults. Clicking on the UploadButton will open your local file system dialogue and let you choose a file — in this case an image (you can add additional code to restrict file types & sizes, both on the client & the server).

Once a file is selected, the :fileChangedCallback on the UploadButton will be triggered. The callback is named fileChanged and is defined in our template’s methods:

fileChanged is triggered when the user selects a file from their local directory

The UploadButton automatically grabs the file data and provides it to the callback. You have to define the callback code yourself, and when first using the UploadButton I didn’t realize I would have to include the FileReader API, which is a native Web API: https://developer.mozilla.org/en-US/docs/Web/API/FileReader

The FileReader API handles reading the content of the files stored on the user’s computer, and can process File and Blob objects. It’s worthwhile giving that MDN page a read to gain a basic understanding of what it’s doing behind the scenes!

The FileReader API is asynchronous, so we add an eventListener to the ‘load’ event and provide a callback for when the file has been read — here I added an arrow function that then saves the image & the file to my template’s data object. Updating the data then triggers the v-if block on the template and will display the new image in the modal to give the user feedback that the image they’ve selected is the one they want.

Once this is done users can either 1.) Cancel the operation 2.) Add a new image or 3.) Save the new image to their profile. The naming conventions I’ve used might not be the best and may change in the future, but that’s the basic gist of it :) We’re interested in what happens when users click the Save button — that invokes the @click callback, named uploadImage. When uploadImage is invoked we do 2 things:

Change the ‘loading’ property on the template’s data to true to trigger any loading animations Dispatch an action with the file to our Vuex store

I like splitting my Vuex store into modules to make it more manageable, so here I dispatch an action to ‘File/uploadFile’ — this triggers the action in a Vuex module I’ve called ‘File’. It could be called anything you want. When the action returns successfully it will switch ‘loading’ back to false & also auto-close the modal, but that’s up to you. Let’s see what happens in between those steps..

Communicating with an Express REST API from Vue

Inside the File module the action is triggered and calls what I’ve named the ‘fileAPI’ inside my Vue app. I like to have a dedicated section in my Vue project to interface with the server — some people might call this ‘Services’, and once again since we’re working with JavaScript the choice is ultimately up to you — find a convention you like and stick with it. Here’s what the File module looks like:

We import the ‘fileAPI’ at the top and call it from the action

Since the call to the server will be asynchronous I’ve made the uploadFile action asynchronous, allowing us to wait for the upload & processing to be done on the server and then to let the client know when it’s all done (or gone wrong!). The fileAPI is also quite simple — it uses Axios to make HTTP requests to the server and then handles the response:

The ‘HTTP’ module is just a wrapper around Axios to handle custom auth headers

In the above example, if you’re using vanilla Axios then where it says HTTP.post that would say Axios.post instead. There are a couple of things to look out for here. Since we’re sending a particular type of data down the wire, we want to let our server know with the ‘Content-Type’ header, and also to create a type of data that Multer will be able to process for us when the file is sent through. To do this, we use the FormData interface: https://developer.mozilla.org/en-US/docs/Web/API/FormData

The FormData interface will create a FormData object out of the image file, assigning it to the key ‘file’, which we’ll use on the server side.

So — deep breath — that takes us close to halfway — we’re successfully grabbing a file from the user’s computer and sending it down the wire to the server — now we have to handle the request, send the file to our Digital Ocean Spaces, ensure everything went according to plan and let the client know!