July 3, 2018

File Uploads with Apollo Server 2.0

Prosper Otemuyiwa
Developer & Technology Advocate
@unicodeveloper
BackendHow-to

Update: May 2020 For our recommendation on how to handle file uploads, read Apollo Server File Uploads Best Practices. In the best practices guide, you’ll learn three different approaches for handling File Uploads: 1 – Multipart Upload Requests 2 – Signed URL Uploads (recommended) 3 – Utilizing an accompanying File Upload/Serving system Continue reading this article for how information on how #1 – Multipart Upload Requests work. For a tutorial on how to implement Multipart Upload Requests, read “GraphQL File Uploads with React Hooks, TypeScript & Amazon S3“.

In web applications, one of the most common requirements is file uploads. For a while now, the question being asked around the GraphQL community is “How does one perform file uploads in GraphQL itself?” The community turned to the GraphQL specification and reference implementation but found no answers because the specification has no provisions for file uploads.

There are several ways you could upload files when using a GraphQL API, for example sending the file to Cloudinary or S3 directly and passing the returned URL through a GraphQL mutation. However, coordinating these multiple requests might be hard to manage. One of the simplest ways of achieving file uploads in a single request is to base64-encode a file and send as a string variable in a mutation. Another option is to send file(s) in the same request as your mutation, which I’ll cover in this post!

Jayden Seric, a JavaScript engineer and GraphQL community member came up with a specification for GraphQL multipart form requests(file uploads) and also provided a reference implementation.

At Apollo, we are committed to making sure you have the best developer experience throughout your GraphQL journey.

We decided to incorporate the much requested file uploads feature into Apollo Server 2.0, thanks to inspiration from the wonderful apollo-upload-server package by Jayden. With GraphQL, we are achieving a single paradigm of data management. Just how Apollo Client merges remote and local data. Apollo Server enables you to combine multiple previously orthogonal data sources into a single paradigm. This post being about the specific case of file uploads and JSON data.

How it works

The upload functionality follows the GraphQL multipart form requests specification. Two parts are needed to make the upload work correctly. The server and the client:

The Client: On the client, file objects are mapped into a mutation and sent to the server in a multipart request.

The Server: The multipart request is received. The server processes it and provides an upload argument to a resolver. In the resolver function, the upload promise resolves an object.

Stay with me! The resolver section will reveal what the object contains and how you can use it.

Server Configuration

The default option for enabling file uploads in Apollo Server 2.0 involves creating a schema and using the Upload type like so:

const { ApolloServer, gql } = require('apollo-server');

const typeDefs = gql`
  type File {
    filename: String!
    mimetype: String!
    encoding: String!
  }
  type Query {
    uploads: [File]
  }
  type Mutation {
    singleUpload(file: Upload!): File!
  }
`;

The first question you’re most likely to ask from the schema observation above is “Where does Upload scalar type come from?” Don’t fret. It is added automatically by Apollo Server.

Apollo Server 2.0 automatically adds the Upload scalar to the schema, when you are not setting the schema manually.

Resolver implementation

Earlier on, I mentioned that the server returns an upload promise that resolves an object. The object contains the following:

  1. stream: The upload stream manages streaming the file(s) to a filesystem or any storage location of your choice. e.g. S3, Azure, Cloudinary, e.t.c.
  2. filename: The name of the uploaded file(s).
  3. mimetype: The MIME type of the file(s) such as text/plainapplication/octet-stream, etc.
  4. encoding: The file encoding such as UTF-8.
const resolvers = {
  Query: {
    files: () => {
      // Return the record of files uploaded from your DB or API or filesystem.
    }
  },
  Mutation: {
    async singleUpload(parent, { file }) {
      const { stream, filename, mimetype, encoding } = await file;

      // 1. Validate file metadata.

      // 2. Stream file contents into cloud storage:
      // https://nodejs.org/api/stream.html

      // 3. Record the file upload in your DB.
      // const id = await recordFile( … )

      return { filename, mimetype, encoding };
    }
  },
};

In the code above, the file can be validated after the promise resolves. If the file size or type is right (depending on the validation technique), it can be streamed into cloud storage like Cloudinary and the returned link can be stored in a database. Otherwise an Apollo error can be thrown within the resolver.

Client Setup

If the user is expected to upload files from an interactive client UI, then you need to install the apollo-upload-client package from npm. And deal with single and multiple files from the client.

Single file upload example from the client:

import gql from 'graphql-tag'
import { Mutation } from 'react-apollo'

export const UPLOAD_FILE = gql`
  mutation uploadFile($file: Upload!) {
    uploadFile(file: $file) {
      filename
    }
  }
`;

const uploadOneFile = () => {
  return (   
    <Mutation mutation={UPLOAD_FILE}>
      {uploadFile => (
        <input
        type="file"
        required
        onChange={({ target: { validity, files: [file] } }) =>
          validity.valid && uploadFile({ variables: { file } });
        }
       />
      )}
    </Mutation>
  );
};

Use <a href="https://developer.mozilla.org/en/docs/Web/API/FileList" target="_blank" rel="noreferrer noopener">FileList</a><a href="https://developer.mozilla.org/en/docs/Web/API/File" target="_blank" rel="noreferrer noopener">File</a><a href="https://developer.mozilla.org/en/docs/Web/API/Blob" target="_blank" rel="noreferrer noopener">Blob</a> or <a href="https://github.com/jaydenseric/apollo-upload-client#react-native" target="_blank" rel="noreferrer noopener">ReactNativeFile</a> instances anywhere within query or mutation input variables to send a GraphQL multipart request. In the example above, we operated a single file upload, thus using <a href="https://developer.mozilla.org/en/docs/Web/API/File" target="_blank" rel="noreferrer noopener">File</a>.

The user selects a file, the client validates the file and immediately sends a multipart request in the same request as the uploadFile mutation to the server.

Check out the file uploads documentation for more information on multiple and blob file uploads from the client.

Try it out

full-stack working example created by Jayden is available on GitHub. Clone and try it out. Feel free to provide feedback.

To keep learning, check out our new best practices guides about topics like schema design, versioning, access control, and more.


Finally, I hope you’ll also join us at the 3rd annual GraphQL Summit on Nov 7–8 in San Francisco. With over 800 attendees expected, it’s the largest GraphQL developer event in the world. Super early bird tickets are selling fast, so register today to reserve your spot!

Written by

Prosper Otemuyiwa

Follow

Writer of all things Technical & Magical. Software Craftsman.

Read more by Prosper Otemuyiwa

Stay in our orbit!

Become an Apollo insider and get first access to new features, best practices, and community events. Oh, and no junk mail. Ever.

Make this article better!

Was this post helpful? Have suggestions? Consider so we can improve it for future readers ✨.

Similar posts

November 18, 2020

SDL Support in Apollo Android

by Martin Bonnin

Company