File upload from a Nuxt.js client to an AdonisJS RESTful API server using axios

Preamble

AdonisJs documentation shows how to upload files to the server using the HTML5 <form> element. But there are cases where axios comes more handy. So let us see how to upload files from a Nuxt.js client application to an AdonisJS RESTful API server with axios. I shared this project on my Github profile.

Project setup

In my demo, the client and server code are set apart. My client application is handled by Nuxt.js:

yarn create nuxt-app client

and the server by AdonisJs:

adonis new server --api-only

On my Github project, I use Vuetify.js and Eslint.

Nuxt.js client code

Here is where the main code is located:

components/
├── FileUpload.vue
└── MainPage.vue
pages/
└── index.vue
layouts/
└── default.vue

And this is the simple user interface I want to create: file upload

Basically I like to have MainPage.vue component to wrap all my other components; in this case I need only the FileUpload.vue component:

<template>
  <file-upload />
</template>

<script>
import FileUpload from '@/components/FileUpload.vue'

export default {
  name: 'MainPage',
  components: {
    FileUpload
  }
}
</script>

The essential part of the code is what the FileUpload.vue component contains. So I show here the full code and then will explain the main points:

 1 <template>
 2   <v-container
 3     grid-list-md
 4     text-xs-center
 5     fill-height>
 6     <v-layout
 7       row
 8       wrap
 9       align-center>
10       <v-flex
11         xs6
12         offset-xs3>
13         <v-text-field
14           v-model="photoName"
15           name="photo"
16           outline
17           background-color="blue"
18           color="blue"
19           label="Select image"
20           append-icon="attach_file"
21           @click="selectImage"/>
22         <input
23           ref="image"
24           class="hide-input"
25           type="file"
26           accept="image/*"
27           @change="imageSelected">
28         <v-btn
29           class="upload-button"
30           color="indigo"
31           @click="upload_photo">
32           Upload
33           <v-icon
34             right
35             color="white">
36             cloud_upload
37           </v-icon>
38         </v-btn>
39       </v-flex>
40     </v-layout>
41   </v-container>
42 </template>
43 
44 <script>
45 export default {
46   name: 'FileUpload',
47   data: () =>({
48     photo: '',
49     photoName: ''
50   }),
51   methods: {
52     selectImage() {
53       this.photo = this.$refs.image.click()
54     },
55     imageSelected(e) {
56       this.$emit('input', e.target.files[0])
57       this.photo = this.$refs.image.files[0]
58       this.photoName = this.photo.name
59     },
60     async upload_photo() {
61       let formData = new FormData()
62       formData.append('file',  this.photo)
63       let url = 'http://127.0.0.1:3333/upload'
64       let config = {
65 	headers: {
66           'content-type': 'multipart/form-data'
67 	}
68       }
69       await this.$axios({
70       	method: 'post',
71       	url: url,
72       	data:  formData,
73       	config: config
74       })
75       
76     }
77   }
78 }
79 </script>
80 
81 <style scoped>
82 .hide-input {
83     display: none;
84 }
85 *{
86     text-transform: none !important;
87 }
88 .upload-button {
89     border-radius: 50px;
90     color: white;
91 }
92 </style>
As Vuetify.js which I am using here (listed in the dependencies of the project) does not have a component which behaves as the HTML5 <input> element, I need to hide this later one when displaying the <v-text-field /> component. This is the traditional simple but efficient trick usually used in this case; then we trigger a click on this hidden file input as follows:

imageSelected(e) {
  this.photo = this.$refs.image.files[0]
},

My input file element is referenced with image, that is why we need to look to the references available in this DOM template with it:

<input
  ref="image"
  class="hide-input"
  type="file"
  accept="image/*"
  @change="imageSelected">

In my particular case, I am interested in uploading images only, that is why I set accept="image/*". The main thing not to forget in the AJAX request is to declare the content-type header. I think the rest of the code is self explanatory:

async upload_photo() {
  let formData = new FormData()
  formData.append('file',  this.photo)
  let url = 'http://127.0.0.1:3333/upload' // This is the endpoint of my REST API on the server 
  // below is equivalent to the enctype="multipart/form-data" we use in the <form> element
  let config = {
    headers: {
      'content-type': 'multipart/form-data'
    }
  }
  await this.$axios({
    method: 'post',
    url: url,
    data:  formData,
    config: config
  })
}

If you are a one-liner, the asynchronous code above (the await part) can be written concisely as follows:

await this.$axios.$post(url, formData, config)

AdonisJs REST API server code

On the server, I first set the endpoint in start/routes.js:

Route.post('/upload', 'PhotoController.upload')

Then I created the corresponding controller:

adonis make:controller PhotoController

Inside this controller (app/Controllers/Http/PhotoController.js), I received the object File() sent by the axios POST request in my client code by inspecting the request object of AdonisJs: const photo = request.file('file'). Note that I use file to reference its key in the FormData() object which contains it. Below I rename the file with the current time of the server’s machine:

'use strict'

const Helpers = use('Helpers')

class PhotoController {
  async upload( {request, response} ) {
    const photo = request.file('file')
    await photo.move(Helpers.tmpPath('photos'), {
      name: new Date().getTime() +'.'+avatar.subtype,
      overwrite: true
    })
  }
}

module.exports = PhotoController

If you upload a photo, you will find it on the server, precisely in the folder /tmp/photos. Note that you have to enable CORS for the requests to be acceped. You can do that by setting cors: true in config/cors.js file. For larger images, set the limit value of maxSize to whatever you want in /config/bodyParser.js which is 20mb by default.