Marvin Rabe

File Upload with Vue, Apollo Client and GraphQL

Our customer requires a file upload to store files on the server. It took me a while to figure everything out by myself. So I decided to share with you how to implement a file upload in Vue with GraphQL (via Vue Apollo).

Preparing the Server

First you need to have a GraphQL server that supports the GraphQL multipart request specification. I will not explain how to implement the server logic in this article, because it is different for any server you might use. You have to figure this out on your own. But probably it is already documented somewhere.

If you are using Laravel you might have a look at Lighthouse. Implementing the server-side upload logic is really easy with it. How to do it is explained in the documentation.

Writing the Query

When you have finished the implementation of the mutation on your server, it is time to write the query. Assuming your mutation field is called upload the result may look like this:

mutation ($file: Upload!){
    upload(file: $file) {
        id
    }
}

I store it as upload.graphql for later reference. Note the Upload type is provided by your server. You might have to change this.

Designing a Vue component

Now we create a simple Vue component. I decided on handling the upload on the change event. You might choose otherwise. But using the mutation will still be the same.

As you can see I am using vue-apollo. This is a library which simplifies the usage of the Apollo Client in Vue somewhat.

<template>
    <input @change="upload" type="file"/>
</template>

<script>
  export default {
    methods: {
      upload ({ target: { files = [] } }) {
        if (!files.length) {
          return
        }

        this.$apollo
          .mutate({
            mutation: require('./graphql/upload.graphql'),
            variables: {
              file: files[0]
            },
            context: {
              hasUpload: true
            }
          })
      }
    }
  }
</script>

But when I ran this component for the first time, even though I implemented the server properly and passed the files to the query as expected, I got this error:

Variable "$file" got invalid value []; Expected type Upload; Could not get uploaded file, be sure to conform to GraphQL multipart request specification: https://github.com/jaydenseric/graphql-multipart-request-spec

This is because the Apollo Client does not support multipart request. So there is no way to do this right now. That’s bad luck…

But wait! Some Apollo Client shenanigans

Apollo Client does not support multipart requests but it is possible to swap the HttpLink with a custom upload link. Then it will work as expected.

First off all, if you are still using Apollo Boost now its time to migrate to the Apollo Client. I know that Apollo Boost is really handy for beginning with GraphQL, but you are limited in your options. So do this now. There is even a migration guide.

So you are using the Apollo Client. Probably you have something that looks like this:

const client = new ApolloClient({
  link: ApolloLink.from([
    // ...
    new HttpLink({
      uri: '/graphql'
    })
  ])
})

We will replace the HttpLink with a custom link that supports file uploads. Install the library:

yarn add apollo-upload-client

Then you can change the HttpLink with the upload link like this:

import { createUploadLink } from 'apollo-upload-client'

const client = new ApolloClient({
  link: ApolloLink.from([
    // ...
    createUploadLink({
      uri: '/graphql'
    })
  ])
})

Now the file upload should work fine.

But I am using Batching!

Well I do too. Sadly you cannot have more than one terminating link. That’s the point of a terminating link. It is terminating. Instead of using the upload link directly you have to do some trickery:

const httpOptions = {
  uri: '/graphql'
}

const httpLink = ApolloLink.split(
  operation => operation.getContext().hasUpload,
  createUploadLink(httpOptions),
  new BatchHttpLink(httpOptions)
)

const client = new ApolloClient({
  link: ApolloLink.from([
    // ...
    httpLink
  ])
})

As you can see we swap the terminating link based on the context. This works like a railway switch. If it is an upload use the upload link, otherwise use BatchHttpLink.

One last detail: You have to tell Apollo that a mutation is an upload. Otherwise the BatchHttpLink will be used which obviously does not support file uploads. Set hasUpload in the context of your mutation in your Vue upload method:

  upload ({ target: { files = [] } }) {
    // ...

    this.$apollo
      .mutate({
        // ...
        context: {
          hasUpload: true // Important!
        }
      })
  }

But please note that you do not have batching for file uploads. But every other query gets batched like before. A small trade-off, but worth it in my opinion.

Multiple files upload

Of course it is still possible to upload multiple files at once. Just write an endpoint that supports an [Upload] array. Instead of one file you have to pass multiple to the mutation. Everything else is still the same.

Conclusion

It is a shame that multipart requests are not supported by Apollo Client out of the box. But there is at least a working solution to fix this. I hope someday Apollo supports this right from the beginning.