Here's How You Upload Files in GraphQL...both Easy and Hard

It doesn't take a long to realize uploading files in GraphQL is a major pain point. File uploads are generally complicated, even in REST APIs. It's often flat out recommended to consider switching back to REST when uploading files, but is really that bad? I have been researching this for sometime now. Here's how to upload files in GraphQL.

The most viable option for GraphQL file uploads seems to be Jayden Seric's multipart request extension if you prefer to maintain a single GraphQL endpoint and pass in your files through GraphQL Mutations. Alternatively you can maintain a separate endpoint to POST upload to (usually implemented with REST HTTP methods). A third is using third-party external storage such as S3 URLS in your mutations.

Uploading files in GraphQL is not that common. These is perhaps because there is no official support and the GraphQL documentation is oddly silent about file uploads.

It's often suggested that simply creating and endpoint (usually in REST) besides your GraphQL to post files to will usually suffice. But what if you want to maintain only one GraphQL endpoint?

Let's kick this is by first going over how REST does file uploads and then ease into uploading files to GraphQL Servers using this background.

Uploading Files in REST

One key area REST beats GraphQL is file uploads. REST leverages HTTP for handling file uploads such as images.

A typical REST image upload looks as follows:

        
POST /images HTTP/1.1
Authentication: Bearer < token >
Host: localhost:4000
Content-Type: image/jpeg
Content-Length: 1254

raw image content
        
      

URL based uploads are handled in the same way using application/json requests.

        
POST /images HTTP/1.1
Host: localhost:4000
Content-Type: image/jpeg
Content-Length: 244

{
  "image_url" : "https://cdn.example.org/image.png"
}
        
      

Uploading files in REST often takes three distinct approaches:

Direct file uploads

This is just uploading by referencing the file by name. The JSON data is usually sent along with the image but it can be split into a different request.

Uploading using file metadata

This method is commonly associated with multipart uploads. The files are split by the MultipartBoundry boundary separators. Multipart uploads are often considered tacky hack because requests have a tendency to get big and messy very quickly (and the fields are also split).

Uploading from an external URL

This is what did in the second second request above using https://cdn.example.org/image.png.

A multipart upload would looking looks something like this.

      
POST /images HTTP/1.1
Host: localhost:4000
Authentication: Bearer < token >
Content-Type: multipart/form-data; boundary=MultipartBoundry
Accept-Encoding: gzip, deflate

--MultipartBoundry
Content-Disposition: form-data; name="image"; filename="458485515151024_454541324960893_3451511151369966555525_n.jpg"
Content-Type: image/jpeg

raw image content
--MultipartBoundry
Content-Disposition: form-data; name="imageJsonData"
Content-Type: application/json

--MultipartBoundry--
      
    

How To Upload Files in GraphQL

Uploading file in GraphQL in not trivial. First GraphQL does not leverage HTTP methods as REST does. Then there's the single endpoint.

Let's see some methods of uploading files in GraphQL.

Uploading with GraphQL Mutations

GraphQL does not allow raw files in mutations. It only handles serialized data.

There are a few workarounds for getting over this. Some of the most useful are:

Uploading via REST Endpoints in GraphQL Mutations

Inside graphql mutations it is possible to upload files in graphql using a separate REST API.

Using this method adds another layer of complexity. Files have to be uploaded to REST first, then the resulting upload URL is passed in the GraphQL mutation. This is usually a slow process and there are two servers to manage now.

The REST API uses methods outlined previously to upload the file.

Uploading Files as Base64 Encoded Strings

Base64 encoded strings can be passed in with GraphQL mutations.

The encoded strings are usually large than their binary counterparts but a third.

The encoding of strings can also become resource intensive pretty quickly and it is sometimes fraught with errors.

Uploading file using External URLs (such as AWS S3)

Another way to upload files to GrapQL is using S3. Once a file is uploaded to S3 and a file url id generate, the URL can be used in a Graph Mutation to store the file in a GraphQL Server.

Uploading Files using apollo-upload-server

apollo-upload-server is a library that allows you to upload files in graphql mutations without needing to create a REST endpoint to handle these uploads.

It uses an extension called the multipart request spec to handle file transfers.

The files to be uploaded can then be nested anywhere in GraphQL mutations using variables as follows:

      
{
  query: `
    mutation($image: Upload!) {
      uploadImage(image: $image) {
        id
      }
    }
  `,
  variables: {
    image: Image // image.jpg
  }
}
      
    

The file is added just like any other mutation parameter.

Apollo Upload is installed with the apollo-upload-server npm package and implemented in Apollo Server and Express as follows:

      
import { apolloUploadExpress } from 'apollo-upload-server'

...

app.use(
  '/graphql',
  bodyParser.json(),
  apolloUploadExpress(/* Options */),
  graphqlExpress(/* … */)

      
    

The options are used for setting maximum files (maxFiles), file upload sizes (maxFileSize) and field sizes (maxFieldSize).

A file upload scalar is them added to types and resolvers as follows:

      
import { makeExecutableSchema } from 'graphql-tools'
import { apolloUploadExpress, GraphQLUpload } from 'apollo-upload-server'
-*
const schema = makeExecutableSchema({
  typeDefs: `scalar Upload`,
  resolvers: { Upload: GraphQLUpload }
})
      
    

Rolling your Own Uploader with Multipart Requests

Another option would be to roll on your multi-part requests. This would be instead of using apollo-upload-server (and apollo-upload-client) although the two approaches have a lot of similarities e.g. the fact that they leverage GraphQL-request.

Jayden Seric multipart extension also has some advantages over the rolling your own approach such as file upload streams in resolvers and the ability to abort file uploads.

To roll your own uploader, you would use GraphQL-request's ability to send arbitrary data instead of the regular POST-request - this is how youn send a multipart request.

For multipart requests, GraphQL needs a query field.

On our GraphQL server, the multipart-request is read into a request object which is then passed into resolver context.

The client would also need a code NetworkInterface to handle multipart request.