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:

1
yarn create nuxt-app client

and the server by AdonisJs:

1
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:

1
2
3
4
5
6
7
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:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
<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>
45export 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:

1
2
3
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:

1
2
3
4
5
6
<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:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
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:

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

AdonisJs REST API server code

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

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

Then I created the corresponding controller:

1
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:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
'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.