GraphQL and file upload using React and Node.js

Dilip Kumar
5 min readAug 5, 2018

Recently I was implementing file upload feature using GraphQL. After exploring more I realized that now we have following two approach to implement it.

  1. Use of apollo-upload-server with express or koa or happi or other App server as middleware
  2. Use of apollo-upload-server with apollo-server

Though both are pretty much similar but little different. This post is going to discuss these two approaches for Server side GraphQL implementation for file upload.

Basics of file upload

HTML input with type file is used to upload single or multiple files to server. File upload form uses multipart/form-data encoding type and send payload in boundary format to send the file data to server. File upload process has following parts

  • input file type HTML control. This allow user to select file from local machine.
  • HTML form with encoding type as multipart/form-data which uses selected files and create payload with multiple parts as below
POST https://localhost:8000/api/upload HTTP/1.1
Content-Type: multipart/form-data; boundary=xxBOUNDARYxx
Content-Length: {POST body length in bytes}

--xxBOUNDARYxx
Content-Disposition: form-data; name="profileImage"; filename="my_new_profile_image.png"
Content-Type: image/png

{image file data bytes}
--xxBOUNDARYxx--
  • Server API to handle multipart/form-data and either write file to local disk or perform some business logic as per need.

In this post, I am going to discuss how to use React.js , Node.js and Apollo GraphQL to upload file.

GraphQL File Upload API for Server Side

GraphQL multipart request specification

multipart request for GraphQL has been documented very well in https://github.com/jaydenseric/graphql-multipart-request-spec . I am going to summarize it in brief in below.

GraphQL multipart request payload is divided into following three boundaries

  • operations : A JSON encoded operations object with files replaced with null
--------------------------cec8e8123c05ba25
Content-Disposition: form-data; name="operations"
{ "query": "mutation ($file: Upload!) { uploadSingleFile(file: $file) { id } }", "variables": { "file": null } }
  • map: A JSON encoded map of where files occurred in the operations. For each file, the key is the file multipart form field name and the value is an array of operations paths.
--------------------------cec8e8123c05ba25
Content-Disposition: form-data; name="map"

{ "0": ["variables.file"] }
  • File fields: Each file extracted from the operations object with a unique, arbitrary field name.
--------------------------cec8e8123c05ba25
Content-Disposition: form-data; name="0"; filename="a.txt"
Content-Type: text/plain

Alpha file content.

--------------------------cec8e8123c05ba25--

GraphQL Multipart Example

GraphQL Operation:

{
query: `
mutation($files: [Upload!]!) {
multipleUpload(files: $files) {
id
}
}
`,
variables: {
files: [
File, // b.txt
File // c.txt
]
}
}

Using cURL request:

curl localhost:3001/graphql \
-F operations='{ "query": "mutation($files: [Upload!]!) { multipleUpload(files: $files) { id } }", "variables": { "files": [null, null] } }' \
-F map='{ "0": ["variables.files.0"], "1": ["variables.files.1"] }' \
-F 0=@b.txt \
-F 1=@c.txt

Request Payload:

--------------------------ec62457de6331cad
Content-Disposition: form-data; name="operations"

{ "query": "mutation($files: [Upload!]!) { multipleUpload(files: $files) { id } }", "variables": { "files": [null, null] } }
--------------------------ec62457de6331cad
Content-Disposition: form-data; name="map"

{ "0": ["variables.files.0"], "1": ["variables.files.1"] }
--------------------------ec62457de6331cad
Content-Disposition: form-data; name="0"; filename="b.txt"
Content-Type: text/plain

Bravo file content.

--------------------------ec62457de6331cad
Content-Disposition: form-data; name="1"; filename="c.txt"
Content-Type: text/plain

Charlie file content.

--------------------------ec62457de6331cad--

Server Side GraphQL implementation for File upload as middleware for App Server

Note: This post will use single file upload as reference to discuss the concepts.

Following are components to support file upload at GraphQL API.

  • GraphQLUpload as schema type for Upload which is type for file
# index.graphql
scalar Upload
type File {
id: ID!
path: String!
filename: String!
mimetype: String!
encoding: String!
}
type Mutation {
uploadSingleFile(file: Upload!): File!
}
// index.js for resolverconst { GraphQLUpload } = require('apollo-upload-server');const resolvers = {
Upload: GraphQLUpload,
Mutation: {
uploadSingleFile: async (root, { file }) => {
const { stream, mimetype } = await file; // Now use stream to either write file at local disk or CDN
}
}
};
  • Add middleware to App server. Following is example to add for Koa server
const { apolloUploadKoa } = require('apollo-upload-server');const app = new Koa();
const router = new KoaRouter();
// Middleware to support file upload for graphql
app.use(apolloUploadKoa());

Server Side GraphQL implementation for File upload with apollo-server

File upload is not by default supported for express or koa or other node.js app server. However this feature is by default supported with apollo-server. Therefore following are not required with apollo-server

  1. No need to declare Upload scaler in schema
  2. No need to declare Upload in resolver
  3. No need to define middleware in app server

Following are details needed to support file upload with apollo-server

  • Define operation in schema
# index.graphqltype File {
id: ID!
path: String!
filename: String!
mimetype: String!
encoding: String!
}
type Mutation {
uploadSingleFile(file: Upload!): File!
}
  • Define resolver
const resolvers = {
Mutation: {
uploadSingleFile: async (root, { file }) => {
const { stream, mimetype } = await file;// Now use stream to either write file at local disk or CDN
}
}
};

GraphQL file upload for React Client

apollo-upload-client module is used for React component to upload file using GraphQL API. Following are two parts to use file upload for client side.

  • ApolloProvider setup with apollo-upload-client
import React from 'react';
import ReactDOM from 'react-dom';
import { ApolloProvider } from 'react-apollo';
import { HttpLink, InMemoryCache, ApolloClient } from 'apollo-client-preset';
import { ApolloLink } from 'apollo-link';
import { createUploadLink } from 'apollo-upload-client';

const uploadLink = createUploadLink({ uri: 'http://localhost:4000/graphql' });
const httpLink = new HttpLink({ uri: 'http://localhost:4000/graphql' });
const cache = new InMemoryCache();

// apollo client setup
const client = new ApolloClient({
cache,
link: ApolloLink.from([uploadLink, link])
})
ReactDOM.render(
<ApolloProvider client={client}>
<App />
</ApolloProvider>,
document.getElementById('root'),
)
  • Use of Mutation with input file to call GraphQL API
import React from 'react';
import PropTypes from 'prop-types';
import { Mutation } from 'react-apollo';
import gql from 'graphql-tag'
const UPLOAD_FILE = gql`
mutation($file: Upload!) {
uploadSingleFile(file: $file) {
path
}
}`;

const handleChange = async ( event, mutation ) => {
const {
target: {
validity,
files: [file],
}
} = event;

if (validity.valid) {
// Call graphql API
const { data: { uploadSingleFile } } = await mutation({
mutation: UPLOAD_FILE,
variables: { file },
fetchPolicy: 'no-cache',
});
// Use uploadSingleFile response
}
};

const UploadFile = ({ onChange, ...rest }) => {
return (
<Mutation mutation={UPLOAD_FILE} fetchPolicy="no-cache">
{ (mutation, { loading }) => (
<input
type="file"
required
onChange={event => handleChange(event, mutation)} />
/>
) }
</Mutation>
);
};

--

--